mirror of
https://github.com/Radarr/Radarr
synced 2026-01-24 16:32:41 +01:00
refactor: extract BaseMediaCrudController for Book/Audiobook (#139)
- Create BaseMediaCrudController with common CRUD patterns - Extract shared validation setup (path, quality, title) - Move Create, Update, Delete endpoints to base class - BookController: 219 -> 168 lines (-51) - AudiobookController: 227 -> 177 lines (-50) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: admin <admin@ardentleatherworks.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
209fd28062
commit
2b42b33d3a
3 changed files with 181 additions and 165 deletions
|
|
@ -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<AudiobookResource, Audiobook>,
|
||||
public class AudiobookController : BaseMediaCrudController<AudiobookResource, Audiobook>,
|
||||
IHandle<AudiobookAddedEvent>,
|
||||
IHandle<AudiobookEditedEvent>,
|
||||
IHandle<AudiobooksDeletedEvent>,
|
||||
|
|
@ -31,6 +30,9 @@ public class AudiobookController : RestControllerWithSignalR<AudiobookResource,
|
|||
private readonly IHierarchicalMonitoringService _monitoringService;
|
||||
private readonly IAudiobookStatisticsService _audiobookStatisticsService;
|
||||
|
||||
protected override IBaseMediaService<Audiobook> 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<AudiobookResource> GetAudiobooks(int? authorId = null, int? seriesId = null, int? bookId = null, string narrator = null)
|
||||
{
|
||||
|
|
@ -120,27 +121,6 @@ public List<AudiobookResource> 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<AudiobookResource> AddAudiobook([FromBody] AudiobookResource audiobookResource)
|
||||
{
|
||||
var audiobook = _audiobookService.AddAudiobook(audiobookResource.ToModel());
|
||||
return Created(audiobook.Id);
|
||||
}
|
||||
|
||||
[RestPutById]
|
||||
[Consumes("application/json")]
|
||||
[Produces("application/json")]
|
||||
public ActionResult<AudiobookResource> 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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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<BookResource, Book>,
|
||||
public class BookController : BaseMediaCrudController<BookResource, Book>,
|
||||
IHandle<BookAddedEvent>,
|
||||
IHandle<BookEditedEvent>,
|
||||
IHandle<BooksDeletedEvent>,
|
||||
|
|
@ -31,6 +29,9 @@ public class BookController : RestControllerWithSignalR<BookResource, Book>,
|
|||
private readonly IHierarchicalMonitoringService _monitoringService;
|
||||
private readonly IBookStatisticsService _bookStatisticsService;
|
||||
|
||||
protected override IBaseMediaService<Book> 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<BookResource> GetBooks(int? authorId = null, int? seriesId = null)
|
||||
{
|
||||
|
|
@ -112,27 +112,6 @@ public List<BookResource> 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<BookResource> AddBook([FromBody] BookResource bookResource)
|
||||
{
|
||||
var book = _bookService.AddBook(bookResource.ToModel());
|
||||
return Created(book.Id);
|
||||
}
|
||||
|
||||
[RestPutById]
|
||||
[Consumes("application/json")]
|
||||
[Produces("application/json")]
|
||||
public ActionResult<BookResource> 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)
|
||||
{
|
||||
|
|
|
|||
117
src/Radarr.Api.V3/MediaItems/BaseMediaCrudController.cs
Normal file
117
src/Radarr.Api.V3/MediaItems/BaseMediaCrudController.cs
Normal file
|
|
@ -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<TResource, TModel> : RestControllerWithSignalR<TResource, TModel>
|
||||
where TResource : RestResource, new()
|
||||
where TModel : ModelBase, new()
|
||||
{
|
||||
protected abstract IBaseMediaService<TModel> 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<TResource> 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<TResource> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue