From 449caa12e36a4810eff741cb1089a66f9aa7c578 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 24 Nov 2025 21:25:19 -0800 Subject: [PATCH] Add v5 root folder endpoints --- .../RootFolders/RootFolderService.cs | 1 + .../RootFolders/RootFolderController.cs | 73 +++++++++++++++++++ .../RootFolders/RootFolderResource.cs | 53 ++++++++++++++ 3 files changed, 127 insertions(+) create mode 100644 src/Sonarr.Api.V5/RootFolders/RootFolderController.cs create mode 100644 src/Sonarr.Api.V5/RootFolders/RootFolderResource.cs diff --git a/src/NzbDrone.Core/RootFolders/RootFolderService.cs b/src/NzbDrone.Core/RootFolders/RootFolderService.cs index 277290cae..d2202664e 100644 --- a/src/NzbDrone.Core/RootFolders/RootFolderService.cs +++ b/src/NzbDrone.Core/RootFolders/RootFolderService.cs @@ -204,6 +204,7 @@ private void GetDetails(RootFolder rootFolder, Dictionary seriesPat if (_diskProvider.FolderExists(rootFolder.Path)) { rootFolder.Accessible = true; + rootFolder.IsEmpty = _diskProvider.FolderEmpty(rootFolder.Path); rootFolder.FreeSpace = _diskProvider.GetAvailableSpace(rootFolder.Path); rootFolder.TotalSpace = _diskProvider.GetTotalSize(rootFolder.Path); rootFolder.UnmappedFolders = GetUnmappedFolders(rootFolder.Path, seriesPaths); diff --git a/src/Sonarr.Api.V5/RootFolders/RootFolderController.cs b/src/Sonarr.Api.V5/RootFolders/RootFolderController.cs new file mode 100644 index 000000000..19b9a3aec --- /dev/null +++ b/src/Sonarr.Api.V5/RootFolders/RootFolderController.cs @@ -0,0 +1,73 @@ +using FluentValidation; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.RootFolders; +using NzbDrone.Core.Validation.Paths; +using NzbDrone.SignalR; +using Sonarr.Http; +using Sonarr.Http.Extensions; +using Sonarr.Http.REST; +using Sonarr.Http.REST.Attributes; + +namespace Sonarr.Api.V5.RootFolders; + +[V5ApiController] +public class RootFolderController : RestControllerWithSignalR +{ + private readonly IRootFolderService _rootFolderService; + + public RootFolderController(IRootFolderService rootFolderService, + IBroadcastSignalRMessage signalRBroadcaster, + RootFolderValidator rootFolderValidator, + PathExistsValidator pathExistsValidator, + MappedNetworkDriveValidator mappedNetworkDriveValidator, + RecycleBinValidator recycleBinValidator, + StartupFolderValidator startupFolderValidator, + SystemFolderValidator systemFolderValidator, + FolderWritableValidator folderWritableValidator) + : base(signalRBroadcaster) + { + _rootFolderService = rootFolderService; + + SharedValidator.RuleFor(c => c.Path) + .Cascade(CascadeMode.Stop) + .IsValidPath() + .SetValidator(rootFolderValidator) + .SetValidator(mappedNetworkDriveValidator) + .SetValidator(startupFolderValidator) + .SetValidator(recycleBinValidator) + .SetValidator(pathExistsValidator) + .SetValidator(systemFolderValidator) + .SetValidator(folderWritableValidator); + } + + protected override RootFolderResource GetResourceById(int id) + { + var timeout = Request?.GetBooleanQueryParameter("timeout", true) ?? true; + + return _rootFolderService.Get(id, timeout).ToResource(); + } + + [RestPostById] + [Consumes("application/json")] + public ActionResult CreateRootFolder([FromBody] RootFolderResource rootFolderResource) + { + var model = rootFolderResource.ToModel(); + + return Created(_rootFolderService.Add(model).Id); + } + + [HttpGet] + [Produces("application/json")] + public List GetRootFolders() + { + return _rootFolderService.AllWithUnmappedFolders().ToResource(); + } + + [RestDeleteById] + public ActionResult DeleteFolder(int id) + { + _rootFolderService.Remove(id); + + return NoContent(); + } +} diff --git a/src/Sonarr.Api.V5/RootFolders/RootFolderResource.cs b/src/Sonarr.Api.V5/RootFolders/RootFolderResource.cs new file mode 100644 index 000000000..f73245246 --- /dev/null +++ b/src/Sonarr.Api.V5/RootFolders/RootFolderResource.cs @@ -0,0 +1,53 @@ +using NzbDrone.Common.Extensions; +using NzbDrone.Core.RootFolders; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V5.RootFolders; + +public class RootFolderResource : RestResource +{ + public string? Path { get; set; } + public bool Accessible { get; set; } + public bool IsEmpty { get; set; } + public long? FreeSpace { get; set; } + public long? TotalSpace { get; set; } + + public List UnmappedFolders { get; set; } = []; +} + +public static class RootFolderResourceMapper +{ + public static RootFolderResource ToResource(this RootFolder model) + { + return new RootFolderResource + { + Id = model.Id, + Path = model.Path.GetCleanPath(), + Accessible = model.Accessible, + IsEmpty = model.IsEmpty, + FreeSpace = model.FreeSpace, + TotalSpace = model.TotalSpace, + UnmappedFolders = model.UnmappedFolders + }; + } + + public static RootFolder ToModel(this RootFolderResource resource) + { + return new RootFolder + { + Id = resource.Id, + + Path = resource.Path + + // Accessible + // IsEmpty + // FreeSpace + // UnmappedFolders + }; + } + + public static List ToResource(this IEnumerable models) + { + return models.Select(ToResource).ToList(); + } +}