1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-04-18 21:35:27 -04:00

Add v5 command endpoints

This commit is contained in:
Mark McDowall
2025-12-02 20:31:11 -08:00
parent 227db9ef39
commit dd12b9e076
2 changed files with 248 additions and 0 deletions

View File

@@ -0,0 +1,131 @@
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common.Composition;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.MediaFiles.EpisodeImport.Manual;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.ProgressMessaging;
using NzbDrone.SignalR;
using Sonarr.Http;
using Sonarr.Http.REST;
using Sonarr.Http.REST.Attributes;
using Sonarr.Http.Validation;
using Debouncer = NzbDrone.Common.TPL.Debouncer;
namespace Sonarr.Api.V5.Commands;
[V5ApiController]
public class CommandController : RestControllerWithSignalR<CommandResource, CommandModel>, IHandle<CommandUpdatedEvent>
{
private readonly IManageCommandQueue _commandQueueManager;
private readonly KnownTypes _knownTypes;
private readonly Debouncer _debouncer;
private readonly Dictionary<int, CommandResource> _pendingUpdates;
private readonly CommandPriorityComparer _commandPriorityComparer = new();
public CommandController(IManageCommandQueue commandQueueManager,
IBroadcastSignalRMessage signalRBroadcaster,
KnownTypes knownTypes)
: base(signalRBroadcaster)
{
_commandQueueManager = commandQueueManager;
_knownTypes = knownTypes;
_debouncer = new Debouncer(SendUpdates, TimeSpan.FromSeconds(0.1));
_pendingUpdates = new Dictionary<int, CommandResource>();
PostValidator.RuleFor(c => c.Name).NotBlank();
}
protected override CommandResource GetResourceById(int id)
{
return _commandQueueManager.Get(id).ToResource();
}
[RestPostById]
[Consumes("application/json")]
[Produces("application/json")]
public ActionResult<CommandResource> StartCommand([FromBody] CommandResource commandResource)
{
var commandType =
_knownTypes.GetImplementations(typeof(Command))
.Single(c => c.Name.Replace("Command", "")
.Equals(commandResource.Name, StringComparison.InvariantCultureIgnoreCase));
Request.Body.Seek(0, SeekOrigin.Begin);
using (var reader = new StreamReader(Request.Body))
{
var body = reader.ReadToEnd();
var priority = commandType == typeof(ManualImportCommand)
? CommandPriority.High
: CommandPriority.Normal;
var command = STJson.Deserialize(body, commandType) as Command;
if (command == null)
{
throw new BadRequestException("Invalid command body");
}
command.SuppressMessages = !command.SendUpdatesToClient;
command.SendUpdatesToClient = true;
command.ClientUserAgent = Request.Headers["UserAgent"];
var trackedCommand = _commandQueueManager.Push(command, priority, CommandTrigger.Manual);
return Created(trackedCommand.Id);
}
}
[HttpGet]
[Produces("application/json")]
public List<CommandResource> GetStartedCommands()
{
return _commandQueueManager.All()
.OrderBy(c => c.Status, _commandPriorityComparer)
.ThenByDescending(c => c.Priority)
.ToResource();
}
[RestDeleteById]
public void CancelCommand(int id)
{
_commandQueueManager.Cancel(id);
}
[NonAction]
public void Handle(CommandUpdatedEvent message)
{
if (message.Command.Body.SendUpdatesToClient)
{
lock (_pendingUpdates)
{
_pendingUpdates[message.Command.Id] = message.Command.ToResource();
}
_debouncer.Execute();
}
}
private void SendUpdates()
{
lock (_pendingUpdates)
{
var pendingUpdates = _pendingUpdates.Values.ToArray();
_pendingUpdates.Clear();
foreach (var pendingUpdate in pendingUpdates)
{
BroadcastResourceChange(ModelAction.Updated, pendingUpdate);
if (pendingUpdate.Name == typeof(MessagingCleanupCommand).Name.Replace("Command", "") &&
pendingUpdate.Status == CommandStatus.Completed)
{
BroadcastResourceChange(ModelAction.Sync);
}
}
}
}
}

View File

@@ -0,0 +1,117 @@
using System.Text.Json.Serialization;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Messaging.Commands;
using Sonarr.Http.REST;
namespace Sonarr.Api.V5.Commands;
public class CommandResource : RestResource
{
public string? Name { get; set; }
public string? CommandName { get; set; }
public string? Message { get; set; }
public Command? Body { get; set; }
public CommandPriority Priority { get; set; }
public CommandStatus Status { get; set; }
public CommandResult Result { get; set; }
public DateTime Queued { get; set; }
public DateTime? Started { get; set; }
public DateTime? Ended { get; set; }
public TimeSpan? Duration { get; set; }
public string? Exception { get; set; }
public CommandTrigger Trigger { get; set; }
public string? ClientUserAgent { get; set; }
[JsonIgnore]
public string? CompletionMessage { get; set; }
public DateTime? StateChangeTime
{
get
{
if (Started.HasValue)
{
return Started.Value;
}
return Ended;
}
set
{
}
}
public bool SendUpdatesToClient
{
get
{
if (Body != null)
{
return Body.SendUpdatesToClient;
}
return false;
}
set
{
}
}
public bool UpdateScheduledTask
{
get
{
if (Body != null)
{
return Body.UpdateScheduledTask;
}
return false;
}
set
{
}
}
public DateTime? LastExecutionTime { get; set; }
}
public static class CommandResourceMapper
{
public static CommandResource ToResource(this CommandModel model)
{
return new CommandResource
{
Id = model.Id,
Name = model.Name,
CommandName = model.Name.SplitCamelCase(),
Message = model.Message,
Body = model.Body,
Priority = model.Priority,
Status = model.Status,
Result = model.Result,
Queued = model.QueuedAt,
Started = model.StartedAt,
Ended = model.EndedAt,
Duration = model.Duration,
Exception = model.Exception,
Trigger = model.Trigger,
ClientUserAgent = UserAgentParser.SimplifyUserAgent(model.Body.ClientUserAgent),
CompletionMessage = model.Body.CompletionMessage,
LastExecutionTime = model.Body.LastExecutionTime
};
}
public static List<CommandResource> ToResource(this IEnumerable<CommandModel> models)
{
return models.Select(ToResource).ToList();
}
}