diff --git a/src/Radarr.Api.V3/Audiobooks/AudiobookController.cs b/src/Radarr.Api.V3/Audiobooks/AudiobookController.cs index 42ff7a854e..66053336d3 100644 --- a/src/Radarr.Api.V3/Audiobooks/AudiobookController.cs +++ b/src/Radarr.Api.V3/Audiobooks/AudiobookController.cs @@ -1,26 +1,25 @@ using System.Collections.Generic; using System.Linq; -using FluentValidation; using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; using NzbDrone.Core.Audiobooks; using NzbDrone.Core.Audiobooks.Events; using NzbDrone.Core.AudiobookStats; using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.MediaItems; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Monitoring; using NzbDrone.Core.RootFolders; using NzbDrone.Core.Validation; using NzbDrone.Core.Validation.Paths; using NzbDrone.SignalR; +using Radarr.Api.V3.MediaItems; using Radarr.Http; -using Radarr.Http.REST; -using Radarr.Http.REST.Attributes; namespace Radarr.Api.V3.Audiobooks { [V3ApiController] - public class AudiobookController : RestControllerWithSignalR, + public class AudiobookController : BaseMediaCrudController, IHandle, IHandle, IHandle, @@ -31,6 +30,9 @@ public class AudiobookController : RestControllerWithSignalR MediaService => _audiobookService; + protected override IRootFolderService RootFolderService => _rootFolderService; + public AudiobookController(IBroadcastSignalRMessage signalRBroadcaster, IAudiobookService audiobookService, IRootFolderService rootFolderService, @@ -49,35 +51,34 @@ public AudiobookController(IBroadcastSignalRMessage signalRBroadcaster, _monitoringService = monitoringService; _audiobookStatisticsService = audiobookStatisticsService; - SharedValidator.RuleFor(s => s.Path).Cascade(CascadeMode.Stop) - .IsValidPath() - .SetValidator(rootFolderValidator) - .SetValidator(mappedNetworkDriveValidator) - .SetValidator(recycleBinValidator) - .SetValidator(systemFolderValidator) - .When(s => s.Path.IsNotNullOrWhiteSpace()); - - PostValidator.RuleFor(s => s.Path).Cascade(CascadeMode.Stop) - .NotEmpty() - .IsValidPath() - .When(s => s.RootFolderPath.IsNullOrWhiteSpace()); - PostValidator.RuleFor(s => s.RootFolderPath).Cascade(CascadeMode.Stop) - .NotEmpty() - .IsValidPath() - .SetValidator(rootFolderExistsValidator) - .When(s => s.Path.IsNullOrWhiteSpace()); - - PutValidator.RuleFor(s => s.Path).Cascade(CascadeMode.Stop) - .NotEmpty() - .IsValidPath(); - - SharedValidator.RuleFor(s => s.QualityProfileId).Cascade(CascadeMode.Stop) - .ValidId() - .SetValidator(qualityProfileExistsValidator); - - PostValidator.RuleFor(s => s.Title).NotEmpty(); + SetupPathValidation(rootFolderValidator, mappedNetworkDriveValidator, recycleBinValidator, systemFolderValidator, rootFolderExistsValidator); + SetupQualityValidation(qualityProfileExistsValidator); + SetupTitleValidation(); } + protected override string GetPath(AudiobookResource resource) => resource.Path; + protected override string GetRootFolderPath(AudiobookResource resource) => resource.RootFolderPath; + protected override int GetQualityProfileId(AudiobookResource resource) => resource.QualityProfileId; + protected override string GetTitle(AudiobookResource resource) => resource.Title; + + protected override AudiobookResource MapToResource(Audiobook audiobook) + { + if (audiobook == null) + { + return null; + } + + var resource = audiobook.ToResource(); + resource.RootFolderPath = _rootFolderService.GetBestRootFolderPath(resource.Path); + resource.EffectivelyMonitored = _monitoringService.IsEffectivelyMonitored(audiobook); + FetchAndLinkAudiobookStatistics(resource); + + return resource; + } + + protected override Audiobook ResourceToModel(AudiobookResource resource) => resource.ToModel(); + protected override Audiobook ApplyResourceToModel(AudiobookResource resource, Audiobook audiobook) => resource.ToModel(audiobook); + [HttpGet] public List GetAudiobooks(int? authorId = null, int? seriesId = null, int? bookId = null, string narrator = null) { @@ -120,27 +121,6 @@ public List GetAudiobooks(int? authorId = null, int? seriesId return resources; } - protected override AudiobookResource GetResourceById(int id) - { - var audiobook = _audiobookService.GetAudiobook(id); - return MapToResource(audiobook); - } - - private AudiobookResource MapToResource(Audiobook audiobook) - { - if (audiobook == null) - { - return null; - } - - var resource = audiobook.ToResource(); - resource.RootFolderPath = _rootFolderService.GetBestRootFolderPath(resource.Path); - resource.EffectivelyMonitored = _monitoringService.IsEffectivelyMonitored(audiobook); - FetchAndLinkAudiobookStatistics(resource); - - return resource; - } - private void FetchAndLinkAudiobookStatistics(AudiobookResource resource) { LinkAudiobookStatistics(resource, _audiobookStatisticsService.AudiobookStatistics(resource.Id)); @@ -164,36 +144,6 @@ private static void LinkAudiobookStatistics(AudiobookResource resource, Audioboo resource.SizeOnDisk = audiobookStatistics.SizeOnDisk; } - [RestPostById] - [Consumes("application/json")] - [Produces("application/json")] - public ActionResult AddAudiobook([FromBody] AudiobookResource audiobookResource) - { - var audiobook = _audiobookService.AddAudiobook(audiobookResource.ToModel()); - return Created(audiobook.Id); - } - - [RestPutById] - [Consumes("application/json")] - [Produces("application/json")] - public ActionResult UpdateAudiobook([FromBody] AudiobookResource audiobookResource) - { - var audiobook = _audiobookService.GetAudiobook(audiobookResource.Id); - var updatedAudiobook = _audiobookService.UpdateAudiobook(audiobookResource.ToModel(audiobook)); - var resource = MapToResource(updatedAudiobook); - - BroadcastResourceChange(ModelAction.Updated, resource); - - return Ok(resource); - } - - [RestDeleteById] - public ActionResult DeleteAudiobook(int id, bool deleteFiles = false) - { - _audiobookService.DeleteAudiobook(id, deleteFiles); - return NoContent(); - } - [NonAction] public void Handle(AudiobookAddedEvent message) { diff --git a/src/Radarr.Api.V3/Books/BookController.cs b/src/Radarr.Api.V3/Books/BookController.cs index ae97c510c6..eafcd55184 100644 --- a/src/Radarr.Api.V3/Books/BookController.cs +++ b/src/Radarr.Api.V3/Books/BookController.cs @@ -1,26 +1,24 @@ using System.Collections.Generic; using System.Linq; -using FluentValidation; using Microsoft.AspNetCore.Mvc; -using NzbDrone.Common.Extensions; using NzbDrone.Core.Books; using NzbDrone.Core.Books.Events; using NzbDrone.Core.BookStats; using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.MediaItems; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Monitoring; using NzbDrone.Core.RootFolders; using NzbDrone.Core.Validation; using NzbDrone.Core.Validation.Paths; using NzbDrone.SignalR; +using Radarr.Api.V3.MediaItems; using Radarr.Http; -using Radarr.Http.REST; -using Radarr.Http.REST.Attributes; namespace Radarr.Api.V3.Books { [V3ApiController] - public class BookController : RestControllerWithSignalR, + public class BookController : BaseMediaCrudController, IHandle, IHandle, IHandle, @@ -31,6 +29,9 @@ public class BookController : RestControllerWithSignalR, private readonly IHierarchicalMonitoringService _monitoringService; private readonly IBookStatisticsService _bookStatisticsService; + protected override IBaseMediaService MediaService => _bookService; + protected override IRootFolderService RootFolderService => _rootFolderService; + public BookController(IBroadcastSignalRMessage signalRBroadcaster, IBookService bookService, IRootFolderService rootFolderService, @@ -49,35 +50,34 @@ public BookController(IBroadcastSignalRMessage signalRBroadcaster, _monitoringService = monitoringService; _bookStatisticsService = bookStatisticsService; - SharedValidator.RuleFor(s => s.Path).Cascade(CascadeMode.Stop) - .IsValidPath() - .SetValidator(rootFolderValidator) - .SetValidator(mappedNetworkDriveValidator) - .SetValidator(recycleBinValidator) - .SetValidator(systemFolderValidator) - .When(s => s.Path.IsNotNullOrWhiteSpace()); - - PostValidator.RuleFor(s => s.Path).Cascade(CascadeMode.Stop) - .NotEmpty() - .IsValidPath() - .When(s => s.RootFolderPath.IsNullOrWhiteSpace()); - PostValidator.RuleFor(s => s.RootFolderPath).Cascade(CascadeMode.Stop) - .NotEmpty() - .IsValidPath() - .SetValidator(rootFolderExistsValidator) - .When(s => s.Path.IsNullOrWhiteSpace()); - - PutValidator.RuleFor(s => s.Path).Cascade(CascadeMode.Stop) - .NotEmpty() - .IsValidPath(); - - SharedValidator.RuleFor(s => s.QualityProfileId).Cascade(CascadeMode.Stop) - .ValidId() - .SetValidator(qualityProfileExistsValidator); - - PostValidator.RuleFor(s => s.Title).NotEmpty(); + SetupPathValidation(rootFolderValidator, mappedNetworkDriveValidator, recycleBinValidator, systemFolderValidator, rootFolderExistsValidator); + SetupQualityValidation(qualityProfileExistsValidator); + SetupTitleValidation(); } + protected override string GetPath(BookResource resource) => resource.Path; + protected override string GetRootFolderPath(BookResource resource) => resource.RootFolderPath; + protected override int GetQualityProfileId(BookResource resource) => resource.QualityProfileId; + protected override string GetTitle(BookResource resource) => resource.Title; + + protected override BookResource MapToResource(Book book) + { + if (book == null) + { + return null; + } + + var resource = book.ToResource(); + resource.RootFolderPath = _rootFolderService.GetBestRootFolderPath(resource.Path); + resource.EffectivelyMonitored = _monitoringService.IsEffectivelyMonitored(book); + FetchAndLinkBookStatistics(resource); + + return resource; + } + + protected override Book ResourceToModel(BookResource resource) => resource.ToModel(); + protected override Book ApplyResourceToModel(BookResource resource, Book book) => resource.ToModel(book); + [HttpGet] public List GetBooks(int? authorId = null, int? seriesId = null) { @@ -112,27 +112,6 @@ public List GetBooks(int? authorId = null, int? seriesId = null) return resources; } - protected override BookResource GetResourceById(int id) - { - var book = _bookService.GetBook(id); - return MapToResource(book); - } - - private BookResource MapToResource(Book book) - { - if (book == null) - { - return null; - } - - var resource = book.ToResource(); - resource.RootFolderPath = _rootFolderService.GetBestRootFolderPath(resource.Path); - resource.EffectivelyMonitored = _monitoringService.IsEffectivelyMonitored(book); - FetchAndLinkBookStatistics(resource); - - return resource; - } - private void FetchAndLinkBookStatistics(BookResource resource) { LinkBookStatistics(resource, _bookStatisticsService.BookStatistics(resource.Id)); @@ -156,36 +135,6 @@ private static void LinkBookStatistics(BookResource resource, BookStatistics boo resource.SizeOnDisk = bookStatistics.SizeOnDisk; } - [RestPostById] - [Consumes("application/json")] - [Produces("application/json")] - public ActionResult AddBook([FromBody] BookResource bookResource) - { - var book = _bookService.AddBook(bookResource.ToModel()); - return Created(book.Id); - } - - [RestPutById] - [Consumes("application/json")] - [Produces("application/json")] - public ActionResult UpdateBook([FromBody] BookResource bookResource) - { - var book = _bookService.GetBook(bookResource.Id); - var updatedBook = _bookService.UpdateBook(bookResource.ToModel(book)); - var resource = MapToResource(updatedBook); - - BroadcastResourceChange(ModelAction.Updated, resource); - - return Ok(resource); - } - - [RestDeleteById] - public ActionResult DeleteBook(int id, bool deleteFiles = false) - { - _bookService.DeleteBook(id, deleteFiles); - return NoContent(); - } - [NonAction] public void Handle(BookAddedEvent message) { diff --git a/src/Radarr.Api.V3/MediaItems/BaseMediaCrudController.cs b/src/Radarr.Api.V3/MediaItems/BaseMediaCrudController.cs new file mode 100644 index 0000000000..545c6b5b65 --- /dev/null +++ b/src/Radarr.Api.V3/MediaItems/BaseMediaCrudController.cs @@ -0,0 +1,117 @@ +using FluentValidation; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.MediaItems; +using NzbDrone.Core.RootFolders; +using NzbDrone.Core.Validation; +using NzbDrone.Core.Validation.Paths; +using NzbDrone.SignalR; +using Radarr.Http.REST; +using Radarr.Http.REST.Attributes; + +namespace Radarr.Api.V3.MediaItems +{ + public abstract class BaseMediaCrudController : RestControllerWithSignalR + where TResource : RestResource, new() + where TModel : ModelBase, new() + { + protected abstract IBaseMediaService MediaService { get; } + protected abstract IRootFolderService RootFolderService { get; } + + protected BaseMediaCrudController(IBroadcastSignalRMessage signalRBroadcaster) + : base(signalRBroadcaster) + { + } + + protected void SetupPathValidation( + RootFolderValidator rootFolderValidator, + MappedNetworkDriveValidator mappedNetworkDriveValidator, + RecycleBinValidator recycleBinValidator, + SystemFolderValidator systemFolderValidator, + RootFolderExistsValidator rootFolderExistsValidator) + { + SharedValidator.RuleFor(s => GetPath(s)).Cascade(CascadeMode.Stop) + .IsValidPath() + .SetValidator(rootFolderValidator) + .SetValidator(mappedNetworkDriveValidator) + .SetValidator(recycleBinValidator) + .SetValidator(systemFolderValidator) + .When(s => GetPath(s).IsNotNullOrWhiteSpace()); + + PostValidator.RuleFor(s => GetPath(s)).Cascade(CascadeMode.Stop) + .NotEmpty() + .IsValidPath() + .When(s => GetRootFolderPath(s).IsNullOrWhiteSpace()); + PostValidator.RuleFor(s => GetRootFolderPath(s)).Cascade(CascadeMode.Stop) + .NotEmpty() + .IsValidPath() + .SetValidator(rootFolderExistsValidator) + .When(s => GetPath(s).IsNullOrWhiteSpace()); + + PutValidator.RuleFor(s => GetPath(s)).Cascade(CascadeMode.Stop) + .NotEmpty() + .IsValidPath(); + } + + protected void SetupQualityValidation(QualityProfileExistsValidator qualityProfileExistsValidator) + { + SharedValidator.RuleFor(s => GetQualityProfileId(s)).Cascade(CascadeMode.Stop) + .ValidId() + .SetValidator(qualityProfileExistsValidator); + } + + protected void SetupTitleValidation() + { + PostValidator.RuleFor(s => GetTitle(s)).NotEmpty(); + } + + protected abstract string GetPath(TResource resource); + protected abstract string GetRootFolderPath(TResource resource); + protected abstract int GetQualityProfileId(TResource resource); + protected abstract string GetTitle(TResource resource); + + protected abstract TResource MapToResource(TModel model); + protected abstract TModel ResourceToModel(TResource resource); + protected abstract TModel ApplyResourceToModel(TResource resource, TModel model); + + protected override TResource GetResourceById(int id) + { + var model = MediaService.Get(id); + return MapToResource(model); + } + + [RestPostById] + [Consumes("application/json")] + [Produces("application/json")] + public virtual ActionResult CreateResource([FromBody] TResource resource) + { + var model = ResourceToModel(resource); + var added = MediaService.Add(model); + return Created(added.Id); + } + + [RestPutById] + [Consumes("application/json")] + [Produces("application/json")] + public virtual ActionResult UpdateResource([FromBody] TResource resource) + { + var existingModel = MediaService.Get(resource.Id); + var updatedModel = ApplyResourceToModel(resource, existingModel); + var result = MediaService.Update(updatedModel); + var resultResource = MapToResource(result); + + BroadcastResourceChange(ModelAction.Updated, resultResource); + + return Ok(resultResource); + } + + [RestDeleteById] + public virtual ActionResult DeleteResource(int id, bool deleteFiles = false) + { + MediaService.Delete(id, deleteFiles); + return NoContent(); + } + } +}