Merge branch 'ui-notifications' into develop

Conflicts:
	NzbDrone.Common/NzbDrone.Common.csproj
	UI/Series/Details/SeasonLayout.js
This commit is contained in:
Mark McDowall 2013-09-03 07:18:04 -07:00
commit a7eb331d4e
87 changed files with 1322 additions and 294 deletions

View File

@ -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<CommandStartedEvent>,
IHandleAsync<CommandCompletedEvent>,
IHandleAsync<CommandFailedEvent>
{
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);
}
}
}

View File

@ -2,8 +2,10 @@
using System.Linq; using System.Linq;
using Nancy; using Nancy;
using NzbDrone.Api.Extensions; using NzbDrone.Api.Extensions;
using NzbDrone.Api.Mapping;
using NzbDrone.Common.Composition; using NzbDrone.Common.Composition;
using NzbDrone.Common.Messaging; using NzbDrone.Common.Messaging;
using NzbDrone.Common.Messaging.Tracking;
namespace NzbDrone.Api.Commands namespace NzbDrone.Api.Commands
{ {
@ -11,14 +13,16 @@ namespace NzbDrone.Api.Commands
{ {
private readonly IMessageAggregator _messageAggregator; private readonly IMessageAggregator _messageAggregator;
private readonly IContainer _container; private readonly IContainer _container;
private readonly ITrackCommands _trackCommands;
public CommandModule(IMessageAggregator messageAggregator, IContainer container) public CommandModule(IMessageAggregator messageAggregator, IContainer container, ITrackCommands trackCommands)
{ {
_messageAggregator = messageAggregator; _messageAggregator = messageAggregator;
_container = container; _container = container;
_trackCommands = trackCommands;
Post["/"] = x => RunCommand(ReadResourceFromRequest()); Post["/"] = x => RunCommand(ReadResourceFromRequest());
Get["/"] = x => GetAllCommands();
} }
private Response RunCommand(CommandResource resource) private Response RunCommand(CommandResource resource)
@ -29,9 +33,15 @@ namespace NzbDrone.Api.Commands
.Equals(resource.Command, StringComparison.InvariantCultureIgnoreCase)); .Equals(resource.Command, StringComparison.InvariantCultureIgnoreCase));
dynamic command = Request.Body.FromJson(commandType); 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();
} }
} }
} }

View File

@ -12,7 +12,6 @@ namespace NzbDrone.Api.Extensions
{ {
private static readonly NancyJsonSerializer NancySerializer = new NancyJsonSerializer(); private static readonly NancyJsonSerializer NancySerializer = new NancyJsonSerializer();
public static readonly string LastModified = BuildInfo.BuildDateTime.ToString("r"); public static readonly string LastModified = BuildInfo.BuildDateTime.ToString("r");
public static T FromJson<T>(this Stream body) where T : class, new() public static T FromJson<T>(this Stream body) where T : class, new()
@ -25,7 +24,6 @@ namespace NzbDrone.Api.Extensions
return (T)FromJson(body, type); return (T)FromJson(body, type);
} }
public static object FromJson(this Stream body, Type type) public static object FromJson(this Stream body, Type type)
{ {
var reader = new StreamReader(body, true); var reader = new StreamReader(body, true);

View File

@ -10,6 +10,7 @@ using NzbDrone.Common.Instrumentation;
using NzbDrone.Common.Messaging; using NzbDrone.Common.Messaging;
using NzbDrone.Core.Instrumentation; using NzbDrone.Core.Instrumentation;
using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.ProgressMessaging;
using TinyIoC; using TinyIoC;
namespace NzbDrone.Api namespace NzbDrone.Api
@ -29,14 +30,12 @@ namespace NzbDrone.Api
{ {
_logger.Info("Starting NzbDrone API"); _logger.Info("Starting NzbDrone API");
RegisterPipelines(pipelines); RegisterPipelines(pipelines);
container.Resolve<DatabaseTarget>().Register(); container.Resolve<DatabaseTarget>().Register();
container.Resolve<IEnableBasicAuthInNancy>().Register(pipelines); container.Resolve<IEnableBasicAuthInNancy>().Register(pipelines);
container.Resolve<IMessageAggregator>().PublishEvent(new ApplicationStartedEvent()); container.Resolve<IMessageAggregator>().PublishEvent(new ApplicationStartedEvent());
ApplicationPipelines.OnError.AddItemToEndOfPipeline(container.Resolve<NzbDroneErrorPipeline>().HandleException); ApplicationPipelines.OnError.AddItemToEndOfPipeline(container.Resolve<NzbDroneErrorPipeline>().HandleException);
} }
@ -48,10 +47,8 @@ namespace NzbDrone.Api
{ {
registerNancyPipeline.Register(pipelines); registerNancyPipeline.Register(pipelines);
} }
} }
protected override TinyIoCContainer GetApplicationContainer() protected override TinyIoCContainer GetApplicationContainer()
{ {
return _tinyIoCContainer; return _tinyIoCContainer;

View File

@ -83,8 +83,12 @@
<Compile Include="ClientSchema\SelectOption.cs" /> <Compile Include="ClientSchema\SelectOption.cs" />
<Compile Include="Commands\CommandModule.cs" /> <Compile Include="Commands\CommandModule.cs" />
<Compile Include="Commands\CommandResource.cs" /> <Compile Include="Commands\CommandResource.cs" />
<Compile Include="Commands\CommandConnection.cs" />
<Compile Include="Config\NamingConfigResource.cs" /> <Compile Include="Config\NamingConfigResource.cs" />
<Compile Include="Config\NamingModule.cs" /> <Compile Include="Config\NamingModule.cs" />
<Compile Include="ProgressMessaging\ProgressMessageConnection.cs" />
<Compile Include="ProgressMessaging\ProgressMessageModule.cs" />
<Compile Include="ProgressMessaging\ProgressMessageResource.cs" />
<Compile Include="EpisodeFiles\EpisodeFileModule.cs" /> <Compile Include="EpisodeFiles\EpisodeFileModule.cs" />
<Compile Include="EpisodeFiles\EpisodeFileResource.cs" /> <Compile Include="EpisodeFiles\EpisodeFileResource.cs" />
<Compile Include="Directories\DirectoryLookupService.cs" /> <Compile Include="Directories\DirectoryLookupService.cs" />

View File

@ -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<NewProgressMessageEvent>
{
public override string Resource
{
get { return "/ProgressMessage"; }
}
public void HandleAsync(NewProgressMessageEvent message)
{
var context = ((ConnectionManager)GlobalHost.ConnectionManager).GetConnection(GetType());
context.Connection.Broadcast(message.ProgressMessage);
}
}
}

View File

@ -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<ProgressMessageResource>
{
public ProgressMessageModule()
{
Get["/"] = x => GetAllMessages();
}
private Response GetAllMessages()
{
return new List<ProgressMessageResource>().AsResponse();
}
}
}

View File

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

View File

@ -48,6 +48,23 @@ namespace NzbDrone.Common.Test.CacheTests
_cachedString.Find("Key").Should().Be("New"); _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] [Test]
public void should_store_null() public void should_store_null()
{ {

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using Moq; using Moq;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common.Messaging; using NzbDrone.Common.Messaging;
using NzbDrone.Common.Messaging.Tracking;
using NzbDrone.Test.Common; using NzbDrone.Test.Common;
namespace NzbDrone.Common.Test.EventingTests namespace NzbDrone.Common.Test.EventingTests
@ -27,6 +28,13 @@ namespace NzbDrone.Common.Test.EventingTests
.Setup(c => c.Build(typeof(IExecute<CommandB>))) .Setup(c => c.Build(typeof(IExecute<CommandB>)))
.Returns(_executorB.Object); .Returns(_executorB.Object);
Mocker.GetMock<ITrackCommands>()
.Setup(c => c.TrackIfNew(It.IsAny<CommandA>()))
.Returns(new TrackedCommand(new CommandA(), ProcessState.Running));
Mocker.GetMock<ITrackCommands>()
.Setup(c => c.TrackIfNew(It.IsAny<CommandB>()))
.Returns(new TrackedCommand(new CommandB(), ProcessState.Running));
} }
[Test] [Test]
@ -34,6 +42,10 @@ namespace NzbDrone.Common.Test.EventingTests
{ {
var commandA = new CommandA(); var commandA = new CommandA();
Mocker.GetMock<ITrackCommands>()
.Setup(c => c.TrackIfNew(commandA))
.Returns(new TrackedCommand(commandA, ProcessState.Running));
Subject.PublishCommand(commandA); Subject.PublishCommand(commandA);
_executorA.Verify(c => c.Execute(commandA), Times.Once()); _executorA.Verify(c => c.Execute(commandA), Times.Once());
@ -55,6 +67,9 @@ namespace NzbDrone.Common.Test.EventingTests
{ {
var commandA = new CommandA(); var commandA = new CommandA();
Mocker.GetMock<ITrackCommands>()
.Setup(c => c.TrackIfNew(commandA))
.Returns(new TrackedCommand(commandA, ProcessState.Running));
Subject.PublishCommand(commandA); Subject.PublishCommand(commandA);
@ -76,17 +91,23 @@ namespace NzbDrone.Common.Test.EventingTests
public class CommandA : ICommand public class CommandA : ICommand
{ {
public String CommandId { get; private set; }
// ReSharper disable UnusedParameter.Local // ReSharper disable UnusedParameter.Local
public CommandA(int id = 0) public CommandA(int id = 0)
// ReSharper restore UnusedParameter.Local // ReSharper restore UnusedParameter.Local
{ {
CommandId = HashUtil.GenerateCommandId();
} }
} }
public class CommandB : ICommand public class CommandB : ICommand
{ {
public String CommandId { get; private set; }
public CommandB()
{
CommandId = HashUtil.GenerateCommandId();
}
} }
} }

View File

@ -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();
}
}
}

View File

@ -69,6 +69,7 @@
<Compile Include="EnvironmentTests\EnvironmentProviderTest.cs" /> <Compile Include="EnvironmentTests\EnvironmentProviderTest.cs" />
<Compile Include="EventingTests\MessageAggregatorCommandTests.cs" /> <Compile Include="EventingTests\MessageAggregatorCommandTests.cs" />
<Compile Include="EventingTests\MessageAggregatorEventTests.cs" /> <Compile Include="EventingTests\MessageAggregatorEventTests.cs" />
<Compile Include="MessagingTests\CommandEqualityComparerFixture.cs" />
<Compile Include="ReflectionExtensions.cs" /> <Compile Include="ReflectionExtensions.cs" />
<Compile Include="PathExtensionFixture.cs" /> <Compile Include="PathExtensionFixture.cs" />
<Compile Include="DiskProviderTests\DiskProviderFixture.cs" /> <Compile Include="DiskProviderTests\DiskProviderFixture.cs" />

View File

@ -28,7 +28,6 @@ namespace NzbDrone.Common.Cache
return GetCache<T>(host, host.FullName); return GetCache<T>(host, host.FullName);
} }
public void Clear() public void Clear()
{ {
_cache.Clear(); _cache.Clear();

View File

@ -61,6 +61,12 @@ namespace NzbDrone.Common.Cache
return value.Object; return value.Object;
} }
public void Remove(string key)
{
CacheItem value;
_store.TryRemove(key, out value);
}
public T Get(string key, Func<T> function, TimeSpan? lifeTime = null) public T Get(string key, Func<T> function, TimeSpan? lifeTime = null)
{ {
Ensure.That(() => key).IsNotNullOrWhiteSpace(); Ensure.That(() => key).IsNotNullOrWhiteSpace();
@ -81,7 +87,6 @@ namespace NzbDrone.Common.Cache
return value; return value;
} }
public void Clear() public void Clear()
{ {
_store.Clear(); _store.Clear();

View File

@ -13,6 +13,7 @@ namespace NzbDrone.Common.Cache
void Set(string key, T value, TimeSpan? lifetime = null); void Set(string key, T value, TimeSpan? lifetime = null);
T Get(string key, Func<T> function, TimeSpan? lifeTime = null); T Get(string key, Func<T> function, TimeSpan? lifeTime = null);
T Find(string key); T Find(string key);
void Remove(string key);
ICollection<T> Values { get; } ICollection<T> Values { get; }
} }

View File

@ -34,31 +34,9 @@ namespace NzbDrone.Common
return String.Format("{0:x8}", mCrc); return String.Format("{0:x8}", mCrc);
} }
public static string GenerateUserId() public static string GenerateCommandId()
{ {
return GenerateId("u"); return GenerateId("c");
}
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;
} }
private static string GenerateId(string prefix) private static string GenerateId(string prefix)

View File

@ -13,7 +13,6 @@ namespace NzbDrone.Common.Instrumentation
return HashUtil.CalculateCrc(hashSeed); return HashUtil.CalculateCrc(hashSeed);
} }
public static string GetFormattedMessage(this LogEventInfo logEvent) public static string GetFormattedMessage(this LogEventInfo logEvent)
{ {
var message = logEvent.FormattedMessage; var message = logEvent.FormattedMessage;

View File

@ -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);
}
}
}

View File

@ -1,12 +0,0 @@
namespace NzbDrone.Common.Messaging
{
public class CommandCompletedEvent : IEvent
{
public ICommand Command { get; private set; }
public CommandCompletedEvent(ICommand command)
{
Command = command;
}
}
}

View File

@ -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<ICommand>
{
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();
}
}
}

View File

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

View File

@ -1,12 +0,0 @@
namespace NzbDrone.Common.Messaging
{
public class CommandExecutedEvent : IEvent
{
public ICommand Command { get; private set; }
public CommandExecutedEvent(ICommand command)
{
Command = command;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,10 @@
using System;
using System.Collections.Generic;
namespace NzbDrone.Common.Messaging namespace NzbDrone.Common.Messaging
{ {
public interface ICommand : IMessage public interface ICommand : IMessage
{ {
String CommandId { get; }
} }
} }

View File

@ -1,4 +1,6 @@
namespace NzbDrone.Common.Messaging using NzbDrone.Common.Messaging.Tracking;
namespace NzbDrone.Common.Messaging
{ {
/// <summary> /// <summary>
/// Enables loosely-coupled publication of events. /// Enables loosely-coupled publication of events.
@ -7,6 +9,8 @@
{ {
void PublishEvent<TEvent>(TEvent @event) where TEvent : class, IEvent; void PublishEvent<TEvent>(TEvent @event) where TEvent : class, IEvent;
void PublishCommand<TCommand>(TCommand command) where TCommand : class, ICommand; void PublishCommand<TCommand>(TCommand command) where TCommand : class, ICommand;
void PublishCommand(string commandType); void PublishCommand(string commandTypeName);
TrackedCommand PublishCommandAsync<TCommand>(TCommand command) where TCommand : class, ICommand;
TrackedCommand PublishCommandAsync(string commandTypeName);
} }
} }

View File

@ -4,7 +4,6 @@
public interface IProcessMessageAsync : IProcessMessage { } public interface IProcessMessageAsync : IProcessMessage { }
public interface IProcessMessage<TMessage> : IProcessMessage { } public interface IProcessMessage<TMessage> : IProcessMessage { }
public interface IProcessMessageAsync<TMessage> : IProcessMessageAsync { } public interface IProcessMessageAsync<TMessage> : IProcessMessageAsync { }

View File

@ -4,6 +4,8 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using NLog; using NLog;
using NzbDrone.Common.EnsureThat; using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.Messaging.Events;
using NzbDrone.Common.Messaging.Tracking;
using NzbDrone.Common.Serializer; using NzbDrone.Common.Serializer;
using NzbDrone.Common.TPL; using NzbDrone.Common.TPL;
@ -13,12 +15,14 @@ namespace NzbDrone.Common.Messaging
{ {
private readonly Logger _logger; private readonly Logger _logger;
private readonly IServiceFactory _serviceFactory; private readonly IServiceFactory _serviceFactory;
private readonly ITrackCommands _trackCommands;
private readonly TaskFactory _taskFactory; private readonly TaskFactory _taskFactory;
public MessageAggregator(Logger logger, IServiceFactory serviceFactory) public MessageAggregator(Logger logger, IServiceFactory serviceFactory, ITrackCommands trackCommands)
{ {
_logger = logger; _logger = logger;
_serviceFactory = serviceFactory; _serviceFactory = serviceFactory;
_trackCommands = trackCommands;
var scheduler = new LimitedConcurrencyLevelTaskScheduler(2); var scheduler = new LimitedConcurrencyLevelTaskScheduler(2);
_taskFactory = new TaskFactory(scheduler); _taskFactory = new TaskFactory(scheduler);
} }
@ -60,7 +64,6 @@ namespace NzbDrone.Common.Messaging
} }
} }
private static string GetEventName(Type eventType) private static string GetEventName(Type eventType)
{ {
if (!eventType.IsGenericType) 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); return string.Format("{0}<{1}>", eventType.Name.Remove(eventType.Name.IndexOf('`')), eventType.GetGenericArguments()[0].Name);
} }
public void PublishCommand<TCommand>(TCommand command) where TCommand : class, ICommand public void PublishCommand<TCommand>(TCommand command) where TCommand : class, ICommand
{ {
Ensure.That(() => command).IsNotNull(); 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<TCommand>(trackedCommand);
}
public void PublishCommand(string commandTypeName)
{
dynamic command = GetCommand(commandTypeName);
PublishCommand(command);
}
public TrackedCommand PublishCommandAsync<TCommand>(TCommand command) where TCommand : class, ICommand
{
Ensure.That(() => command).IsNotNull();
_logger.Trace("Publishing {0}", command.GetType().Name); _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<TCommand>(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<TCommand>(TrackedCommand trackedCommand) where TCommand : class, ICommand
{
var command = (TCommand)trackedCommand.Command;
var handlerContract = typeof(IExecute<>).MakeGenericType(command.GetType());
var handler = (IExecute<TCommand>)_serviceFactory.Build(handlerContract); var handler = (IExecute<TCommand>)_serviceFactory.Build(handlerContract);
_logger.Debug("{0} -> {1}", command.GetType().Name, handler.GetType().Name); _logger.Debug("{0} -> {1}", command.GetType().Name, handler.GetType().Name);
@ -88,30 +145,27 @@ namespace NzbDrone.Common.Messaging
try try
{ {
MappedDiagnosticsContext.Set("CommandId", trackedCommand.Command.CommandId);
PublishEvent(new CommandStartedEvent(trackedCommand));
handler.Execute(command); handler.Execute(command);
sw.Stop(); sw.Stop();
PublishEvent(new CommandCompletedEvent(command));
_trackCommands.Completed(trackedCommand, sw.Elapsed);
PublishEvent(new CommandCompletedEvent(trackedCommand));
} }
catch (Exception e) catch (Exception e)
{ {
PublishEvent(new CommandFailedEvent(command, e)); _trackCommands.Failed(trackedCommand, e);
PublishEvent(new CommandFailedEvent(trackedCommand, e));
throw; throw;
} }
finally finally
{ {
PublishEvent(new CommandExecutedEvent(command)); PublishEvent(new CommandExecutedEvent(trackedCommand));
} }
_logger.Debug("{0} <- {1} [{2}]", command.GetType().Name, handler.GetType().Name, sw.Elapsed.ToString("")); _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);
}
} }
} }

View File

@ -1,13 +1,15 @@
namespace NzbDrone.Common.Messaging using System;
namespace NzbDrone.Common.Messaging
{ {
public class TestCommand : ICommand public class TestCommand : ICommand
{ {
public int Duration { get; set; }
public String CommandId { get; private set; }
public TestCommand() public TestCommand()
{ {
Duration = 4000; Duration = 4000;
} }
public int Duration { get; set; }
} }
} }

View File

@ -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<TrackedCommand> AllTracked();
Boolean ExistingCommand(ICommand command);
TrackedCommand FindExisting(ICommand command);
}
public class TrackCommands : ITrackCommands, IExecute<TrackedCommandCleanupCommand>
{
private readonly ICached<TrackedCommand> _cache;
public TrackCommands(ICacheManger cacheManger)
{
_cache = cacheManger.GetCache<TrackedCommand>(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<TrackedCommand> 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<TrackedCommand> 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);
}
}
}
}

View File

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

View File

@ -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
}
}

View File

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

View File

@ -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();
}
}
}

View File

@ -94,6 +94,14 @@
<Compile Include="Instrumentation\ExceptronTarget.cs" /> <Compile Include="Instrumentation\ExceptronTarget.cs" />
<Compile Include="Instrumentation\NzbDroneLogger.cs" /> <Compile Include="Instrumentation\NzbDroneLogger.cs" />
<Compile Include="Instrumentation\LogTargets.cs" /> <Compile Include="Instrumentation\LogTargets.cs" />
<Compile Include="Instrumentation\LoggerExtensions.cs" />
<Compile Include="Messaging\Tracking\ProcessState.cs" />
<Compile Include="Messaging\Tracking\CommandTrackingService.cs" />
<Compile Include="Messaging\Tracking\ExistingCommand.cs" />
<Compile Include="Messaging\Tracking\TrackedCommand.cs" />
<Compile Include="Messaging\Events\CommandStartedEvent.cs" />
<Compile Include="Messaging\CommandEqualityComparer.cs" />
<Compile Include="Messaging\Tracking\TrackedCommandCleanupCommand.cs" />
<Compile Include="PathEqualityComparer.cs" /> <Compile Include="PathEqualityComparer.cs" />
<Compile Include="Services.cs" /> <Compile Include="Services.cs" />
<Compile Include="TPL\LimitedConcurrencyLevelTaskScheduler.cs" /> <Compile Include="TPL\LimitedConcurrencyLevelTaskScheduler.cs" />
@ -104,9 +112,9 @@
<Compile Include="Instrumentation\LogEventExtensions.cs" /> <Compile Include="Instrumentation\LogEventExtensions.cs" />
<Compile Include="Instrumentation\LogglyTarget.cs" /> <Compile Include="Instrumentation\LogglyTarget.cs" />
<Compile Include="Serializer\Json.cs" /> <Compile Include="Serializer\Json.cs" />
<Compile Include="Messaging\CommandCompletedEvent.cs" /> <Compile Include="Messaging\Events\CommandCompletedEvent.cs" />
<Compile Include="Messaging\CommandStartedEvent.cs" /> <Compile Include="Messaging\Events\CommandExecutedEvent.cs" />
<Compile Include="Messaging\CommandFailedEvent.cs" /> <Compile Include="Messaging\Events\CommandFailedEvent.cs" />
<Compile Include="Messaging\IExecute.cs" /> <Compile Include="Messaging\IExecute.cs" />
<Compile Include="Messaging\ICommand.cs" /> <Compile Include="Messaging\ICommand.cs" />
<Compile Include="Messaging\IMessage.cs" /> <Compile Include="Messaging\IMessage.cs" />

View File

@ -80,6 +80,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Top_Gear.19x06.720p_HDTV_x264-FoV", "Top Gear", 19, 6)] [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("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("(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) public void ParseTitle_single(string postTitle, string title, int seasonNumber, int episodeNumber)
{ {
var result = Parser.Parser.ParseTitle(postTitle); var result = Parser.Parser.ParseTitle(postTitle);

View File

@ -1,8 +1,16 @@
using System;
using NzbDrone.Common;
using NzbDrone.Common.Messaging; using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.DataAugmentation.Scene namespace NzbDrone.Core.DataAugmentation.Scene
{ {
public class UpdateSceneMappingCommand : ICommand public class UpdateSceneMappingCommand : ICommand
{ {
public String CommandId { get; private set; }
public UpdateSceneMappingCommand()
{
CommandId = HashUtil.GenerateCommandId();
}
} }
} }

View File

@ -1,9 +1,17 @@
using NzbDrone.Common.Messaging; using System;
using NzbDrone.Common;
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.IndexerSearch namespace NzbDrone.Core.IndexerSearch
{ {
public class EpisodeSearchCommand : ICommand public class EpisodeSearchCommand : ICommand
{ {
public String CommandId { get; private set; }
public int EpisodeId { get; set; } public int EpisodeId { get; set; }
public EpisodeSearchCommand()
{
CommandId = HashUtil.GenerateCommandId();
}
} }
} }

View File

@ -1,10 +1,18 @@
using NzbDrone.Common.Messaging; using System;
using NzbDrone.Common;
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.IndexerSearch namespace NzbDrone.Core.IndexerSearch
{ {
public class SeasonSearchCommand : ICommand public class SeasonSearchCommand : ICommand
{ {
public String CommandId { get; private set; }
public int SeriesId { get; set; } public int SeriesId { get; set; }
public int SeasonNumber { get; set; } public int SeasonNumber { get; set; }
public SeasonSearchCommand()
{
CommandId = HashUtil.GenerateCommandId();
}
} }
} }

View File

@ -1,9 +1,17 @@
using NzbDrone.Common.Messaging; using System;
using NzbDrone.Common;
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.IndexerSearch namespace NzbDrone.Core.IndexerSearch
{ {
public class SeriesSearchCommand : ICommand public class SeriesSearchCommand : ICommand
{ {
public String CommandId { get; private set; }
public int SeriesId { get; set; } public int SeriesId { get; set; }
public SeriesSearchCommand()
{
CommandId = HashUtil.GenerateCommandId();
}
} }
} }

View File

@ -1,9 +1,16 @@
using NzbDrone.Common.Messaging; using System;
using NzbDrone.Common;
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.Indexers namespace NzbDrone.Core.Indexers
{ {
public class RssSyncCommand : ICommand public class RssSyncCommand : ICommand
{ {
public String CommandId { get; private set; }
public RssSyncCommand()
{
CommandId = HashUtil.GenerateCommandId();
}
} }
} }

View File

@ -1,5 +1,6 @@
using System.Linq; using System.Linq;
using NLog; using NLog;
using NzbDrone.Common.Instrumentation;
using NzbDrone.Common.Messaging; using NzbDrone.Common.Messaging;
using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download; using NzbDrone.Core.Download;
@ -38,7 +39,7 @@ namespace NzbDrone.Core.Indexers
var decisions = _downloadDecisionMaker.GetRssDecision(reports); var decisions = _downloadDecisionMaker.GetRssDecision(reports);
var qualifiedReports = _downloadApprovedReports.DownloadApproved(decisions); 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) public void Execute(RssSyncCommand message)

View File

@ -1,8 +1,16 @@
using NzbDrone.Common.Messaging; using System;
using NzbDrone.Common;
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.Instrumentation.Commands namespace NzbDrone.Core.Instrumentation.Commands
{ {
public class ClearLogCommand : ICommand public class ClearLogCommand : ICommand
{ {
public String CommandId { get; private set; }
public ClearLogCommand()
{
CommandId = HashUtil.GenerateCommandId();
}
} }
} }

View File

@ -1,8 +1,16 @@
using NzbDrone.Common.Messaging; using System;
using NzbDrone.Common;
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.Instrumentation.Commands namespace NzbDrone.Core.Instrumentation.Commands
{ {
public class DeleteLogFilesCommand : ICommand public class DeleteLogFilesCommand : ICommand
{ {
public String CommandId { get; private set; }
public DeleteLogFilesCommand()
{
CommandId = HashUtil.GenerateCommandId();
}
} }
} }

View File

@ -1,8 +1,16 @@
using NzbDrone.Common.Messaging; using System;
using NzbDrone.Common;
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.Instrumentation.Commands namespace NzbDrone.Core.Instrumentation.Commands
{ {
public class TrimLogCommand : ICommand public class TrimLogCommand : ICommand
{ {
public String CommandId { get; private set; }
public TrimLogCommand()
{
CommandId = HashUtil.GenerateCommandId();
}
} }
} }

View File

@ -3,6 +3,8 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using NLog; using NLog;
using NzbDrone.Common.Messaging; using NzbDrone.Common.Messaging;
using NzbDrone.Common.Messaging.Events;
using NzbDrone.Common.Messaging.Tracking;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Configuration.Events; using NzbDrone.Core.Configuration.Events;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
@ -47,7 +49,8 @@ namespace NzbDrone.Core.Jobs
new ScheduledTask{ Interval = 12*60, TypeName = typeof(RefreshSeriesCommand).FullName}, new ScheduledTask{ Interval = 12*60, TypeName = typeof(RefreshSeriesCommand).FullName},
new ScheduledTask{ Interval = 1, TypeName = typeof(DownloadedEpisodesScanCommand).FullName}, new ScheduledTask{ Interval = 1, TypeName = typeof(DownloadedEpisodesScanCommand).FullName},
new ScheduledTask{ Interval = 60, TypeName = typeof(ApplicationUpdateCommand).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(); var currentTasks = _scheduledTaskRepository.All();
@ -76,7 +79,7 @@ namespace NzbDrone.Core.Jobs
public void HandleAsync(CommandExecutedEvent message) 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) if (scheduledTask != null)
{ {

View File

@ -6,5 +6,4 @@ namespace NzbDrone.Core.Lifecycle
{ {
} }
} }

View File

@ -1,13 +1,22 @@
using System;
using NzbDrone.Common;
using NzbDrone.Common.Messaging; using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.MediaFiles.Commands namespace NzbDrone.Core.MediaFiles.Commands
{ {
public class CleanMediaFileDb : ICommand public class CleanMediaFileDb : ICommand
{ {
public String CommandId { get; private set; }
public int SeriesId { get; private set; } public int SeriesId { get; private set; }
public CleanMediaFileDb()
{
CommandId = HashUtil.GenerateCommandId();
}
public CleanMediaFileDb(int seriesId) public CleanMediaFileDb(int seriesId)
{ {
CommandId = HashUtil.GenerateCommandId();
SeriesId = seriesId; SeriesId = seriesId;
} }
} }

View File

@ -1,8 +1,16 @@
using System;
using NzbDrone.Common;
using NzbDrone.Common.Messaging; using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.MediaFiles.Commands namespace NzbDrone.Core.MediaFiles.Commands
{ {
public class CleanUpRecycleBinCommand : ICommand public class CleanUpRecycleBinCommand : ICommand
{ {
public String CommandId { get; private set; }
public CleanUpRecycleBinCommand()
{
CommandId = HashUtil.GenerateCommandId();
}
} }
} }

View File

@ -1,11 +1,16 @@
using System;
using NzbDrone.Common;
using NzbDrone.Common.Messaging; using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.MediaFiles.Commands namespace NzbDrone.Core.MediaFiles.Commands
{ {
public class DownloadedEpisodesScanCommand : ICommand public class DownloadedEpisodesScanCommand : ICommand
{ {
public String CommandId { get; private set; }
public DownloadedEpisodesScanCommand() public DownloadedEpisodesScanCommand()
{ {
CommandId = HashUtil.GenerateCommandId();
} }
} }
} }

View File

@ -1,14 +1,24 @@
using System;
using NzbDrone.Common;
using NzbDrone.Common.Messaging; using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.MediaFiles.Commands namespace NzbDrone.Core.MediaFiles.Commands
{ {
public class RenameSeasonCommand : ICommand public class RenameSeasonCommand : ICommand
{ {
public int SeriesId { get; private set; } public int SeriesId { get; set; }
public int SeasonNumber { get; private set; } public int SeasonNumber { get; set; }
public String CommandId { get; private set; }
public RenameSeasonCommand()
{
CommandId = HashUtil.GenerateCommandId();
}
public RenameSeasonCommand(int seriesId, int seasonNumber) public RenameSeasonCommand(int seriesId, int seasonNumber)
{ {
CommandId = HashUtil.GenerateCommandId();
SeriesId = seriesId; SeriesId = seriesId;
SeasonNumber = seasonNumber; SeasonNumber = seasonNumber;
} }

View File

@ -1,13 +1,22 @@
using System;
using NzbDrone.Common;
using NzbDrone.Common.Messaging; using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.MediaFiles.Commands namespace NzbDrone.Core.MediaFiles.Commands
{ {
public class RenameSeriesCommand : ICommand 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) public RenameSeriesCommand(int seriesId)
{ {
CommandId = HashUtil.GenerateCommandId();
SeriesId = seriesId; SeriesId = seriesId;
} }
} }

View File

@ -1,9 +1,12 @@
using NzbDrone.Common.Messaging; using System;
using NzbDrone.Common;
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.Notifications.Email namespace NzbDrone.Core.Notifications.Email
{ {
public class TestEmailCommand : ICommand public class TestEmailCommand : ICommand
{ {
public String CommandId { get; private set; }
public string Server { get; set; } public string Server { get; set; }
public int Port { get; set; } public int Port { get; set; }
public bool Ssl { get; set; } public bool Ssl { get; set; }
@ -11,5 +14,10 @@ namespace NzbDrone.Core.Notifications.Email
public string Password { get; set; } public string Password { get; set; }
public string From { get; set; } public string From { get; set; }
public string To { get; set; } public string To { get; set; }
public TestEmailCommand()
{
CommandId = HashUtil.GenerateCommandId();
}
} }
} }

View File

@ -1,11 +1,19 @@
using NzbDrone.Common.Messaging; using System;
using NzbDrone.Common;
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.Notifications.Growl namespace NzbDrone.Core.Notifications.Growl
{ {
public class TestGrowlCommand : ICommand public class TestGrowlCommand : ICommand
{ {
public String CommandId { get; private set; }
public string Host { get; set; } public string Host { get; set; }
public int Port { get; set; } public int Port { get; set; }
public string Password { get; set; } public string Password { get; set; }
public TestGrowlCommand()
{
CommandId = HashUtil.GenerateCommandId();
}
} }
} }

View File

@ -1,12 +1,20 @@
using NzbDrone.Common.Messaging; using System;
using NzbDrone.Common;
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.Notifications.Plex namespace NzbDrone.Core.Notifications.Plex
{ {
public class TestPlexClientCommand : ICommand public class TestPlexClientCommand : ICommand
{ {
public String CommandId { get; private set; }
public string Host { get; set; } public string Host { get; set; }
public int Port { get; set; } public int Port { get; set; }
public string Username { get; set; } public string Username { get; set; }
public string Password { get; set; } public string Password { get; set; }
public TestPlexClientCommand()
{
CommandId = HashUtil.GenerateCommandId();
}
} }
} }

View File

@ -1,10 +1,18 @@
using NzbDrone.Common.Messaging; using System;
using NzbDrone.Common;
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.Notifications.Plex namespace NzbDrone.Core.Notifications.Plex
{ {
public class TestPlexServerCommand : ICommand public class TestPlexServerCommand : ICommand
{ {
public String CommandId { get; private set; }
public string Host { get; set; } public string Host { get; set; }
public int Port { get; set; } public int Port { get; set; }
public TestPlexServerCommand()
{
CommandId = HashUtil.GenerateCommandId();
}
} }
} }

View File

@ -1,10 +1,18 @@
using NzbDrone.Common.Messaging; using System;
using NzbDrone.Common;
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.Notifications.Prowl namespace NzbDrone.Core.Notifications.Prowl
{ {
public class TestProwlCommand : ICommand public class TestProwlCommand : ICommand
{ {
public String CommandId { get; private set; }
public string ApiKey { get; set; } public string ApiKey { get; set; }
public int Priority { get; set; } public int Priority { get; set; }
public TestProwlCommand()
{
CommandId = HashUtil.GenerateCommandId();
}
} }
} }

View File

@ -1,10 +1,18 @@
using NzbDrone.Common.Messaging; using System;
using NzbDrone.Common;
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.Notifications.Pushover namespace NzbDrone.Core.Notifications.Pushover
{ {
public class TestPushoverCommand : ICommand public class TestPushoverCommand : ICommand
{ {
public String CommandId { get; private set; }
public string UserKey { get; set; } public string UserKey { get; set; }
public int Priority { get; set; } public int Priority { get; set; }
public TestPushoverCommand()
{
CommandId = HashUtil.GenerateCommandId();
}
} }
} }

View File

@ -1,13 +1,21 @@
using NzbDrone.Common.Messaging; using System;
using NzbDrone.Common;
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.Notifications.Xbmc namespace NzbDrone.Core.Notifications.Xbmc
{ {
public class TestXbmcCommand : ICommand public class TestXbmcCommand : ICommand
{ {
public String CommandId { get; private set; }
public string Host { get; set; } public string Host { get; set; }
public int Port { get; set; } public int Port { get; set; }
public string Username { get; set; } public string Username { get; set; }
public string Password { get; set; } public string Password { get; set; }
public int DisplayTime { get; set; } public int DisplayTime { get; set; }
public TestXbmcCommand()
{
CommandId = HashUtil.GenerateCommandId();
}
} }
} }

View File

@ -219,6 +219,9 @@
<Compile Include="Instrumentation\Commands\DeleteLogFilesCommand.cs" /> <Compile Include="Instrumentation\Commands\DeleteLogFilesCommand.cs" />
<Compile Include="Instrumentation\Commands\TrimLogCommand.cs" /> <Compile Include="Instrumentation\Commands\TrimLogCommand.cs" />
<Compile Include="Instrumentation\DeleteLogFilesService.cs" /> <Compile Include="Instrumentation\DeleteLogFilesService.cs" />
<Compile Include="ProgressMessaging\NewProgressMessageEvent.cs" />
<Compile Include="ProgressMessaging\ProgressMessage.cs" />
<Compile Include="ProgressMessaging\ProgressMessageTarget.cs" />
<Compile Include="Instrumentation\SetLoggingLevel.cs" /> <Compile Include="Instrumentation\SetLoggingLevel.cs" />
<Compile Include="Jobs\TaskManager.cs" /> <Compile Include="Jobs\TaskManager.cs" />
<Compile Include="Lifecycle\ApplicationShutdownRequested.cs" /> <Compile Include="Lifecycle\ApplicationShutdownRequested.cs" />

View File

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

View File

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

View File

@ -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<ApplicationStartedEvent>, IHandle<ApplicationShutdownRequested>
{
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();
}
}
}
}

View File

@ -1,13 +1,17 @@
using NzbDrone.Common.Messaging; using System;
using NzbDrone.Common;
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.Providers namespace NzbDrone.Core.Providers
{ {
public class UpdateXemMappingsCommand : ICommand 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) public UpdateXemMappingsCommand(int? seriesId)
{ {
CommandId = HashUtil.GenerateCommandId();
SeriesId = seriesId; SeriesId = seriesId;
} }
} }

View File

@ -132,8 +132,7 @@ namespace NzbDrone.Core.Providers
catch (Exception ex) catch (Exception ex)
{ {
//TODO: We should increase this back to warn when caching is in place _logger.ErrorException("Error updating scene numbering mappings for: " + series, ex);
_logger.TraceException("Error updating scene numbering mappings for: " + series, ex);
} }
} }

View File

@ -1,13 +1,23 @@
using NzbDrone.Common.Messaging; using System;
using NzbDrone.Common;
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.Tv.Commands namespace NzbDrone.Core.Tv.Commands
{ {
public class RefreshSeriesCommand : ICommand 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) public RefreshSeriesCommand(int? seriesId)
{ {
CommandId = HashUtil.GenerateCommandId();
SeriesId = seriesId; SeriesId = seriesId;
} }
} }

View File

@ -1,8 +1,16 @@
using NzbDrone.Common.Messaging; using System;
using NzbDrone.Common;
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.Update.Commands namespace NzbDrone.Core.Update.Commands
{ {
public class ApplicationUpdateCommand : ICommand public class ApplicationUpdateCommand : ICommand
{ {
public String CommandId { get; private set; }
public ApplicationUpdateCommand()
{
CommandId = HashUtil.GenerateCommandId();
}
} }
} }

View File

@ -1,5 +1,10 @@
using NUnit.Framework; using System.Net;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Api.Commands; using NzbDrone.Api.Commands;
using NzbDrone.Common.Messaging.Tracking;
using NzbDrone.Common.Serializer;
using RestSharp;
namespace NzbDrone.Integration.Test namespace NzbDrone.Integration.Test
{ {
@ -9,7 +14,27 @@ namespace NzbDrone.Integration.Test
[Test] [Test]
public void should_be_able_to_run_rss_sync() 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<TrackedCommand>(response.Content);
trackedCommand.Id.Should().NotBeNullOrEmpty();
} }
} }
} }

View File

@ -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;
});

View File

@ -0,0 +1,8 @@
'use strict';
define(
[
'backbone'
], function (Backbone) {
return Backbone.Model.extend({
});
});

View File

@ -9,9 +9,9 @@ define(
'Series/SeriesCollection', 'Series/SeriesCollection',
'Shared/LoadingView', 'Shared/LoadingView',
'Shared/Messenger', 'Shared/Messenger',
'Commands/CommandController', 'Shared/Actioneer',
'Shared/FormatHelpers' '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({ return Marionette.Layout.extend({
template: 'Episode/Search/LayoutTemplate', template: 'Episode/Search/LayoutTemplate',
@ -39,16 +39,19 @@ define(
e.preventDefault(); e.preventDefault();
} }
CommandController.Execute('episodeSearch', { episodeId: this.model.get('id') });
var series = SeriesCollection.get(this.model.get('seriesId')); var series = SeriesCollection.get(this.model.get('seriesId'));
var seriesTitle = series.get('title'); var seriesTitle = series.get('title');
var season = this.model.get('seasonNumber'); var season = this.model.get('seasonNumber');
var episode = this.model.get('episodeNumber'); var episode = this.model.get('episodeNumber');
var message = seriesTitle + ' - ' + season + 'x' + FormatHelpers.pad(episode, 2); var message = seriesTitle + ' - ' + season + 'x' + FormatHelpers.pad(episode, 2);
Messenger.show({ Actioneer.ExecuteCommand({
message: 'Search started for: ' + message command : 'episodeSearch',
properties : {
episodeId: this.model.get('id')
},
errorMessage: 'Search failed for: ' + message,
startMessage: 'Search started for: ' + message
}); });
App.vent.trigger(App.Commands.CloseModalCommand); App.vent.trigger(App.Commands.CloseModalCommand);

View File

@ -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;
});

View File

@ -0,0 +1,8 @@
'use strict';
define(
[
'backbone'
], function (Backbone) {
return Backbone.Model.extend({
});
});

View File

@ -5,10 +5,12 @@ require(
'marionette', 'marionette',
'Controller', 'Controller',
'Series/SeriesCollection', 'Series/SeriesCollection',
'ProgressMessaging/ProgressMessageCollection',
'Shared/Actioneer',
'Navbar/NavbarView', 'Navbar/NavbarView',
'jQuery/RouteBinder', 'jQuery/RouteBinder',
'jquery' 'jquery'
], function (App, Marionette, Controller, SeriesCollection, NavbarView, RouterBinder, $) { ], function (App, Marionette, Controller, SeriesCollection, ProgressMessageCollection, Actioneer, NavbarView, RouterBinder, $) {
var Router = Marionette.AppRouter.extend({ var Router = Marionette.AppRouter.extend({
@ -42,7 +44,7 @@ require(
RouterBinder.bind(App.Router); RouterBinder.bind(App.Router);
App.navbarRegion.show(new NavbarView()); App.navbarRegion.show(new NavbarView());
$('body').addClass('started'); $('body').addClass('started');
}) });
}); });
return App.Router; return App.Router;

View File

@ -113,6 +113,8 @@ define(
}, },
_setMonitored: function (seasonNumber) { _setMonitored: function (seasonNumber) {
//TODO: use Actioneer?
var self = this; var self = this;
var promise = $.ajax({ var promise = $.ajax({

View File

@ -8,9 +8,8 @@ define(
'Cells/EpisodeTitleCell', 'Cells/EpisodeTitleCell',
'Cells/RelativeDateCell', 'Cells/RelativeDateCell',
'Cells/EpisodeStatusCell', 'Cells/EpisodeStatusCell',
'Commands/CommandController',
'Shared/Actioneer' '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({ return Marionette.Layout.extend({
template: 'Series/Details/SeasonLayoutTemplate', template: 'Series/Details/SeasonLayoutTemplate',
@ -104,8 +103,9 @@ define(
seasonNumber: this.model.get('seasonNumber') seasonNumber: this.model.get('seasonNumber')
}, },
element : this.ui.seasonSearch, 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')) startMessage : 'Search for season {0} started'.format(this.model.get('seasonNumber')),
successMessage: 'Search for season {0} completed'.format(this.model.get('seasonNumber'))
}); });
}, },
@ -151,7 +151,7 @@ define(
seasonNumber: this.model.get('seasonNumber') seasonNumber: this.model.get('seasonNumber')
}, },
element : this.ui.seasonRename, element : this.ui.seasonRename,
failMessage: 'Season rename failed', errorMessage: 'Season rename failed',
context : this, context : this,
onSuccess : this._afterRename onSuccess : this._afterRename
}); });

View File

@ -51,7 +51,7 @@ define(
seasonNumber: this.model.get('seasonNumber') seasonNumber: this.model.get('seasonNumber')
}, },
element : this.ui.seasonSearch, 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')) startMessage: 'Search for season {0} started'.format(this.model.get('seasonNumber'))
}); });
}, },

View File

@ -158,7 +158,7 @@ define(
element : this.ui.rename, element : this.ui.rename,
context : this, context : this,
onSuccess : this._refetchEpisodeFiles, onSuccess : this._refetchEpisodeFiles,
failMessage: 'Series search failed' errorMessage: 'Series search failed'
}); });
}, },
@ -169,7 +169,7 @@ define(
seriesId: this.model.get('id') seriesId: this.model.get('id')
}, },
element : this.ui.search, element : this.ui.search,
failMessage : 'Series search failed', errorMessage: 'Series search failed',
startMessage: 'Search for {0} started'.format(this.model.get('title')) startMessage: 'Search for {0} started'.format(this.model.get('title'))
}); });
}, },

View File

@ -105,7 +105,6 @@ define(
title : 'RSS Sync', title : 'RSS Sync',
icon : 'icon-rss', icon : 'icon-rss',
command : 'rsssync', command : 'rsssync',
successMessage: 'RSS Sync Completed',
errorMessage : 'RSS Sync Failed!' errorMessage : 'RSS Sync Failed!'
}, },
{ {
@ -140,7 +139,6 @@ define(
this._fetchCollection(); this._fetchCollection();
}, },
initialize: function () { initialize: function () {
this.seriesCollection = SeriesCollection; this.seriesCollection = SeriesCollection;
@ -148,7 +146,6 @@ define(
this.listenTo(SeriesCollection, 'remove', this._renderView); this.listenTo(SeriesCollection, 'remove', this._renderView);
}, },
_renderView: function () { _renderView: function () {
if (SeriesCollection.length === 0) { if (SeriesCollection.length === 0) {
@ -164,7 +161,6 @@ define(
} }
}, },
onShow: function () { onShow: function () {
this._showToolbar(); this._showToolbar();
this._renderView(); this._renderView();

View File

@ -66,7 +66,7 @@
<button class="btn pull-left x-back">back</button> <button class="btn pull-left x-back">back</button>
{{/if}} {{/if}}
<button class="btn x-test">test <i class="x-test-icon"/></button> <button class="btn x-test">test <i class="x-test-icon icon-nd-test"/></button>
<button class="btn" data-dismiss="modal">cancel</button> <button class="btn" data-dismiss="modal">cancel</button>
<div class="btn-group"> <div class="btn-group">

View File

@ -6,11 +6,11 @@ define([
'Settings/Notifications/Model', 'Settings/Notifications/Model',
'Settings/Notifications/DeleteView', 'Settings/Notifications/DeleteView',
'Shared/Messenger', 'Shared/Messenger',
'Commands/CommandController', 'Shared/Actioneer',
'Mixins/AsModelBoundView', 'Mixins/AsModelBoundView',
'Form/FormBuilder' 'Form/FormBuilder'
], function (App, Marionette, NotificationModel, DeleteView, Messenger, CommandController, AsModelBoundView) { ], function (App, Marionette, NotificationModel, DeleteView, Messenger, Actioneer, AsModelBoundView) {
var model = Marionette.ItemView.extend({ var model = Marionette.ItemView.extend({
template: 'Settings/Notifications/EditTemplate', template: 'Settings/Notifications/EditTemplate',
@ -70,41 +70,28 @@ define([
var testCommand = this.model.get('testCommand'); var testCommand = this.model.get('testCommand');
if (testCommand) { if (testCommand) {
this.idle = false; this.idle = false;
this.ui.testButton.addClass('disabled');
this.ui.testIcon.addClass('icon-spinner icon-spin');
var properties = {}; var properties = {};
_.each(this.model.get('fields'), function (field) { _.each(this.model.get('fields'), function (field) {
properties[field.name] = field.value; properties[field.name] = field.value;
}); });
var self = this; Actioneer.ExecuteCommand({
var commandPromise = CommandController.Execute(testCommand, properties); command : testCommand,
commandPromise.done(function () { properties : properties,
Messenger.show({ button : this.ui.testButton,
message: 'Notification settings tested successfully' element : this.ui.testIcon,
errorMessage : 'Failed to test notification settings',
successMessage: 'Notification settings tested successfully',
always : this._testOnAlways,
context : this
}); });
});
commandPromise.fail(function (options) {
if (options.readyState === 0 || options.status === 0) {
return;
} }
},
Messenger.show({ _testOnAlways: function () {
message: 'Failed to test notification settings', if (!this.isClosed) {
type : 'error' this.idle = true;
});
});
commandPromise.always(function () {
if (!self.isClosed) {
self.ui.testButton.removeClass('disabled');
self.ui.testIcon.removeClass('icon-spinner icon-spin');
self.idle = true;
}
});
} }
} }
}); });

View File

@ -1,15 +1,30 @@
'use strict'; 'use strict';
define(['Commands/CommandController', 'Shared/Messenger'], define(
function(CommandController, Messenger) { [
return { '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) { ExecuteCommand: function (options) {
options.iconClass = this._getIconClass(options.element); options.iconClass = this._getIconClass(options.element);
this._showStartMessage(options); if (options.button) {
options.button.addClass('disable');
}
this._setSpinnerOnElement(options); this._setSpinnerOnElement(options);
var promise = CommandController.Execute(options.command, options.properties); var promise = CommandController.Execute(options.command, options.properties);
this._handlePromise(promise, options); this._showStartMessage(options, promise);
}, },
SaveModel: function (options) { SaveModel: function (options) {
@ -24,15 +39,7 @@ define(['Commands/CommandController', 'Shared/Messenger'],
_handlePromise: function (promise, options) { _handlePromise: function (promise, options) {
promise.done(function () { promise.done(function () {
if (options.successMessage) { self._onSuccess(options);
Messenger.show({
message: options.successMessage
});
}
if (options.onSuccess) {
options.onSuccess.call(options.context);
}
}); });
promise.fail(function (ajaxOptions) { promise.fail(function (ajaxOptions) {
@ -40,31 +47,46 @@ define(['Commands/CommandController', 'Shared/Messenger'],
return; return;
} }
if (options.failMessage) { self._onError(options);
Messenger.show({
message: options.failMessage,
type : 'error'
});
}
if (options.onError) {
options.onError.call(options.context);
}
}); });
promise.always(function () { promise.always(function () {
self._onComplete(options);
});
},
if (options.leaveIcon) { _handleCommands: function () {
options.element.removeClass('icon-spin'); var self = this;
_.each(this.trackedCommands, function (trackedCommand){
if (trackedCommand.completed === true) {
return;
} }
else { var options = trackedCommand.options;
options.element.addClass(options.iconClass); var command = CommandCollection.find({ 'id': trackedCommand.id });
options.element.removeClass('icon-nd-spinner');
if (!command) {
trackedCommand.completed = true;
self._onError(options, trackedCommand.id);
self._onComplete(options);
return;
} }
if (options.always) { if (command.get('state') === 'completed') {
options.always.call(options.context); trackedCommand.completed = true;
self._onSuccess(options, command.get('id'));
self._onComplete(options);
return;
}
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) { _setSpinnerOnElement: function (options) {
if (!options.element) {
return;
}
if (options.leaveIcon) { if (options.leaveIcon) {
options.element.addClass('icon-spin'); options.element.addClass('icon-spin');
} }
@ -84,12 +110,79 @@ define(['Commands/CommandController', 'Shared/Messenger'],
} }
}, },
_showStartMessage: function (options) { _onSuccess: function (options, id) {
if (options.successMessage) {
Messenger.show({
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) { if (options.startMessage) {
Messenger.show({ Messenger.show({
message: options.startMessage 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();
}); });

View File

@ -13,6 +13,10 @@ define(function () {
options.hideAfter = 5; options.hideAfter = 5;
break; break;
case 'success':
options.hideAfter = 5;
break;
default : default :
options.hideAfter = 0; options.hideAfter = 0;
} }
@ -22,11 +26,11 @@ define(function () {
message : options.message, message : options.message,
type : options.type, type : options.type,
showCloseButton: true, showCloseButton: true,
hideAfter : options.hideAfter hideAfter : options.hideAfter,
id : options.id
}); });
}, },
monitor: function (options) { monitor: function (options) {
if (!options.promise) { if (!options.promise) {

View File

@ -3,9 +3,9 @@ define(
[ [
'app', 'app',
'marionette', 'marionette',
'Commands/CommandController', 'Shared/Actioneer',
'Shared/Messenger' 'Shared/Messenger'
], function (App, Marionette, CommandController, Messenger) { ], function (App, Marionette, Actioneer, Messenger) {
return Marionette.ItemView.extend({ return Marionette.ItemView.extend({
template : 'Shared/Toolbar/ButtonTemplate', template : 'Shared/Toolbar/ButtonTemplate',
@ -19,7 +19,6 @@ define(
icon: '.x-icon' icon: '.x-icon'
}, },
initialize: function () { initialize: function () {
this.storageKey = this.model.get('menuKey') + ':' + this.model.get('key'); this.storageKey = this.model.get('menuKey') + ':' + this.model.get('key');
this.idle = true; this.idle = true;
@ -45,69 +44,20 @@ define(
}, },
invokeCommand: function () { invokeCommand: function () {
//TODO: Use Actioneer to handle icon swapping
var command = this.model.get('command'); var command = this.model.get('command');
if (command) { if (command) {
this.idle = false; this.idle = false;
this.$el.addClass('disabled');
this.ui.icon.addClass('icon-spinner icon-spin');
var self = this; Actioneer.ExecuteCommand({
var commandPromise = CommandController.Execute(command); command : command,
commandPromise.done(function () { button : this.$el,
if (self.model.get('successMessage')) { element : this.ui.icon,
Messenger.show({ errorMessage : this.model.get('errorMessage'),
message: self.model.get('successMessage') successMessage: this.model.get('successMessage'),
always : this._commandAlways,
context : this
}); });
} }
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);
}
});
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);
}
}
}, },
invokeRoute: function () { invokeRoute: function () {
@ -133,8 +83,13 @@ define(
if (callback) { if (callback) {
callback.call(this.model.ownerContext); callback.call(this.model.ownerContext);
} }
},
_commandAlways: function () {
if (!this.isClosed) {
this.idle = true;
}
} }
}); });
}); });

View File

@ -30,10 +30,8 @@ define(
this.left = options.left; this.left = options.left;
this.right = options.right; this.right = options.right;
this.toolbarContext = options.context; this.toolbarContext = options.context;
}, },
onShow: function () { onShow: function () {
if (this.left) { if (this.left) {
_.each(this.left, this._showToolbarLeft, this); _.each(this.left, this._showToolbarLeft, this);
@ -51,7 +49,6 @@ define(
this._showToolbar(element, index, 'right'); this._showToolbar(element, index, 'right');
}, },
_showToolbar: function (buttonGroup, index, position) { _showToolbar: function (buttonGroup, index, position) {
var groupCollection = new ButtonCollection(); var groupCollection = new ButtonCollection();