From cfa42cbfbaba1a1fd1cf03af26f806a8646000be Mon Sep 17 00:00:00 2001 From: Cody Kickertz Date: Tue, 23 Dec 2025 09:14:18 -0600 Subject: [PATCH] refactor: extract BaseMediaEditorController for bulk operations (#140) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create IEditorResource interface for common editor properties - Create BaseMediaEditorController with common bulk edit/delete logic - Extract tag handling (Add/Remove/Replace) to base class - BookEditorController: 92 -> 36 lines (-56) - AudiobookEditorController: 92 -> 36 lines (-56) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: admin Co-authored-by: Claude Opus 4.5 --- .../Audiobooks/AudiobookEditorController.cs | 90 +++------------ .../Audiobooks/AudiobookEditorResource.cs | 5 +- .../Books/BookEditorController.cs | 90 +++------------ src/Radarr.Api.V3/Books/BookEditorResource.cs | 5 +- .../MediaItems/BaseMediaEditorController.cs | 107 ++++++++++++++++++ .../MediaItems/IEditorResource.cs | 15 +++ 6 files changed, 164 insertions(+), 148 deletions(-) create mode 100644 src/Radarr.Api.V3/MediaItems/BaseMediaEditorController.cs create mode 100644 src/Radarr.Api.V3/MediaItems/IEditorResource.cs diff --git a/src/Radarr.Api.V3/Audiobooks/AudiobookEditorController.cs b/src/Radarr.Api.V3/Audiobooks/AudiobookEditorController.cs index e45e937eec..ddfb86f0bf 100644 --- a/src/Radarr.Api.V3/Audiobooks/AudiobookEditorController.cs +++ b/src/Radarr.Api.V3/Audiobooks/AudiobookEditorController.cs @@ -1,92 +1,36 @@ using System.Collections.Generic; -using FluentValidation; -using Microsoft.AspNetCore.Mvc; -using NzbDrone.Common.Extensions; +using FluentValidation.Results; using NzbDrone.Core.Audiobooks; +using Radarr.Api.V3.MediaItems; using Radarr.Http; namespace Radarr.Api.V3.Audiobooks { [V3ApiController("audiobook/editor")] - public class AudiobookEditorController : Controller + public class AudiobookEditorController : BaseMediaEditorController { private readonly IAudiobookService _audiobookService; private readonly AudiobookEditorValidator _audiobookEditorValidator; - public AudiobookEditorController(IAudiobookService audiobookService, - AudiobookEditorValidator audiobookEditorValidator) + public AudiobookEditorController(IAudiobookService audiobookService, AudiobookEditorValidator audiobookEditorValidator) { _audiobookService = audiobookService; _audiobookEditorValidator = audiobookEditorValidator; } - [HttpPut] - public IActionResult SaveAll([FromBody] AudiobookEditorResource resource) - { - var audiobooksToUpdate = _audiobookService.GetAudiobooks(resource.AudiobookIds); + protected override List GetItemsByIds(List ids) => _audiobookService.GetAudiobooks(ids); + protected override List UpdateItems(List items) => _audiobookService.UpdateAudiobooks(items); + protected override void DeleteItems(List ids, bool deleteFiles) => _audiobookService.DeleteAudiobooks(ids, deleteFiles); + protected override ValidationResult ValidateItem(Audiobook item) => _audiobookEditorValidator.Validate(item); + protected override AudiobookResource ToResource(Audiobook model) => model.ToResource(); - foreach (var audiobook in audiobooksToUpdate) - { - if (resource.Monitored.HasValue) - { - audiobook.Monitored = resource.Monitored.Value; - } - - if (resource.QualityProfileId.HasValue) - { - audiobook.QualityProfileId = resource.QualityProfileId.Value; - } - - if (resource.RootFolderPath.IsNotNullOrWhiteSpace()) - { - audiobook.RootFolderPath = resource.RootFolderPath; - } - - if (resource.Tags != null) - { - var newTags = resource.Tags; - var applyTags = resource.ApplyTags; - - switch (applyTags) - { - case ApplyTags.Add: - newTags.ForEach(t => audiobook.Tags.Add(t)); - break; - case ApplyTags.Remove: - newTags.ForEach(t => audiobook.Tags.Remove(t)); - break; - case ApplyTags.Replace: - audiobook.Tags = new HashSet(newTags); - break; - } - } - - var validationResult = _audiobookEditorValidator.Validate(audiobook); - - if (!validationResult.IsValid) - { - throw new ValidationException(validationResult.Errors); - } - } - - var updatedAudiobooks = _audiobookService.UpdateAudiobooks(audiobooksToUpdate); - - var audiobooksResources = new List(updatedAudiobooks.Count); - - foreach (var audiobook in updatedAudiobooks) - { - audiobooksResources.Add(audiobook.ToResource()); - } - - return Ok(audiobooksResources); - } - - [HttpDelete] - public object DeleteAudiobooks([FromBody] AudiobookEditorResource resource) - { - _audiobookService.DeleteAudiobooks(resource.AudiobookIds, resource.DeleteFiles); - - return new { }; - } + protected override bool GetMonitored(Audiobook item) => item.Monitored; + protected override void SetMonitored(Audiobook item, bool monitored) => item.Monitored = monitored; + protected override int GetQualityProfileId(Audiobook item) => item.QualityProfileId; + protected override void SetQualityProfileId(Audiobook item, int qualityProfileId) => item.QualityProfileId = qualityProfileId; + protected override string GetRootFolderPath(Audiobook item) => item.RootFolderPath; + protected override void SetRootFolderPath(Audiobook item, string rootFolderPath) => item.RootFolderPath = rootFolderPath; + protected override HashSet GetTags(Audiobook item) => item.Tags; + protected override void SetTags(Audiobook item, HashSet tags) => item.Tags = tags; } } diff --git a/src/Radarr.Api.V3/Audiobooks/AudiobookEditorResource.cs b/src/Radarr.Api.V3/Audiobooks/AudiobookEditorResource.cs index 1e3b0dd97c..367f0c8c7f 100644 --- a/src/Radarr.Api.V3/Audiobooks/AudiobookEditorResource.cs +++ b/src/Radarr.Api.V3/Audiobooks/AudiobookEditorResource.cs @@ -1,8 +1,9 @@ using System.Collections.Generic; +using Radarr.Api.V3.MediaItems; namespace Radarr.Api.V3.Audiobooks { - public class AudiobookEditorResource + public class AudiobookEditorResource : IEditorResource { public List AudiobookIds { get; set; } public bool? Monitored { get; set; } @@ -12,5 +13,7 @@ public class AudiobookEditorResource public ApplyTags ApplyTags { get; set; } public bool MoveFiles { get; set; } public bool DeleteFiles { get; set; } + + List IEditorResource.Ids => AudiobookIds; } } diff --git a/src/Radarr.Api.V3/Books/BookEditorController.cs b/src/Radarr.Api.V3/Books/BookEditorController.cs index 544c53d1dd..9ccd7b5900 100644 --- a/src/Radarr.Api.V3/Books/BookEditorController.cs +++ b/src/Radarr.Api.V3/Books/BookEditorController.cs @@ -1,92 +1,36 @@ using System.Collections.Generic; -using FluentValidation; -using Microsoft.AspNetCore.Mvc; -using NzbDrone.Common.Extensions; +using FluentValidation.Results; using NzbDrone.Core.Books; +using Radarr.Api.V3.MediaItems; using Radarr.Http; namespace Radarr.Api.V3.Books { [V3ApiController("book/editor")] - public class BookEditorController : Controller + public class BookEditorController : BaseMediaEditorController { private readonly IBookService _bookService; private readonly BookEditorValidator _bookEditorValidator; - public BookEditorController(IBookService bookService, - BookEditorValidator bookEditorValidator) + public BookEditorController(IBookService bookService, BookEditorValidator bookEditorValidator) { _bookService = bookService; _bookEditorValidator = bookEditorValidator; } - [HttpPut] - public IActionResult SaveAll([FromBody] BookEditorResource resource) - { - var booksToUpdate = _bookService.GetBooks(resource.BookIds); + protected override List GetItemsByIds(List ids) => _bookService.GetBooks(ids); + protected override List UpdateItems(List items) => _bookService.UpdateBooks(items); + protected override void DeleteItems(List ids, bool deleteFiles) => _bookService.DeleteBooks(ids, deleteFiles); + protected override ValidationResult ValidateItem(Book item) => _bookEditorValidator.Validate(item); + protected override BookResource ToResource(Book model) => model.ToResource(); - foreach (var book in booksToUpdate) - { - if (resource.Monitored.HasValue) - { - book.Monitored = resource.Monitored.Value; - } - - if (resource.QualityProfileId.HasValue) - { - book.QualityProfileId = resource.QualityProfileId.Value; - } - - if (resource.RootFolderPath.IsNotNullOrWhiteSpace()) - { - book.RootFolderPath = resource.RootFolderPath; - } - - if (resource.Tags != null) - { - var newTags = resource.Tags; - var applyTags = resource.ApplyTags; - - switch (applyTags) - { - case ApplyTags.Add: - newTags.ForEach(t => book.Tags.Add(t)); - break; - case ApplyTags.Remove: - newTags.ForEach(t => book.Tags.Remove(t)); - break; - case ApplyTags.Replace: - book.Tags = new HashSet(newTags); - break; - } - } - - var validationResult = _bookEditorValidator.Validate(book); - - if (!validationResult.IsValid) - { - throw new ValidationException(validationResult.Errors); - } - } - - var updatedBooks = _bookService.UpdateBooks(booksToUpdate); - - var booksResources = new List(updatedBooks.Count); - - foreach (var book in updatedBooks) - { - booksResources.Add(book.ToResource()); - } - - return Ok(booksResources); - } - - [HttpDelete] - public object DeleteBooks([FromBody] BookEditorResource resource) - { - _bookService.DeleteBooks(resource.BookIds, resource.DeleteFiles); - - return new { }; - } + protected override bool GetMonitored(Book item) => item.Monitored; + protected override void SetMonitored(Book item, bool monitored) => item.Monitored = monitored; + protected override int GetQualityProfileId(Book item) => item.QualityProfileId; + protected override void SetQualityProfileId(Book item, int qualityProfileId) => item.QualityProfileId = qualityProfileId; + protected override string GetRootFolderPath(Book item) => item.RootFolderPath; + protected override void SetRootFolderPath(Book item, string rootFolderPath) => item.RootFolderPath = rootFolderPath; + protected override HashSet GetTags(Book item) => item.Tags; + protected override void SetTags(Book item, HashSet tags) => item.Tags = tags; } } diff --git a/src/Radarr.Api.V3/Books/BookEditorResource.cs b/src/Radarr.Api.V3/Books/BookEditorResource.cs index a8a8d3e6d5..f660c088c5 100644 --- a/src/Radarr.Api.V3/Books/BookEditorResource.cs +++ b/src/Radarr.Api.V3/Books/BookEditorResource.cs @@ -1,8 +1,9 @@ using System.Collections.Generic; +using Radarr.Api.V3.MediaItems; namespace Radarr.Api.V3.Books { - public class BookEditorResource + public class BookEditorResource : IEditorResource { public List BookIds { get; set; } public bool? Monitored { get; set; } @@ -12,5 +13,7 @@ public class BookEditorResource public ApplyTags ApplyTags { get; set; } public bool MoveFiles { get; set; } public bool DeleteFiles { get; set; } + + List IEditorResource.Ids => BookIds; } } diff --git a/src/Radarr.Api.V3/MediaItems/BaseMediaEditorController.cs b/src/Radarr.Api.V3/MediaItems/BaseMediaEditorController.cs new file mode 100644 index 0000000000..916bcd5c43 --- /dev/null +++ b/src/Radarr.Api.V3/MediaItems/BaseMediaEditorController.cs @@ -0,0 +1,107 @@ +using System.Collections.Generic; +using FluentValidation; +using FluentValidation.Results; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore; +using Radarr.Http.REST; + +namespace Radarr.Api.V3.MediaItems +{ + public abstract class BaseMediaEditorController : Controller + where TModel : ModelBase, new() + where TResource : RestResource, new() + where TEditorResource : class, IEditorResource + { + protected abstract List GetItemsByIds(List ids); + protected abstract List UpdateItems(List items); + protected abstract void DeleteItems(List ids, bool deleteFiles); + protected abstract ValidationResult ValidateItem(TModel item); + protected abstract TResource ToResource(TModel model); + + protected abstract bool GetMonitored(TModel item); + protected abstract void SetMonitored(TModel item, bool monitored); + protected abstract int GetQualityProfileId(TModel item); + protected abstract void SetQualityProfileId(TModel item, int qualityProfileId); + protected abstract string GetRootFolderPath(TModel item); + protected abstract void SetRootFolderPath(TModel item, string rootFolderPath); + protected abstract HashSet GetTags(TModel item); + protected abstract void SetTags(TModel item, HashSet tags); + + [HttpPut] + public virtual IActionResult SaveAll([FromBody] TEditorResource resource) + { + var itemsToUpdate = GetItemsByIds(resource.Ids); + + foreach (var item in itemsToUpdate) + { + ApplyEditorChanges(item, resource); + + var validationResult = ValidateItem(item); + + if (!validationResult.IsValid) + { + throw new ValidationException(validationResult.Errors); + } + } + + var updatedItems = UpdateItems(itemsToUpdate); + + var resources = new List(updatedItems.Count); + foreach (var item in updatedItems) + { + resources.Add(ToResource(item)); + } + + return Ok(resources); + } + + [HttpDelete] + public virtual IActionResult DeleteAll([FromBody] TEditorResource resource) + { + DeleteItems(resource.Ids, resource.DeleteFiles); + return Ok(new { }); + } + + protected virtual void ApplyEditorChanges(TModel item, TEditorResource resource) + { + if (resource.Monitored.HasValue) + { + SetMonitored(item, resource.Monitored.Value); + } + + if (resource.QualityProfileId.HasValue) + { + SetQualityProfileId(item, resource.QualityProfileId.Value); + } + + if (resource.RootFolderPath.IsNotNullOrWhiteSpace()) + { + SetRootFolderPath(item, resource.RootFolderPath); + } + + if (resource.Tags != null) + { + ApplyTagChanges(item, resource.Tags, resource.ApplyTags); + } + } + + protected void ApplyTagChanges(TModel item, List newTags, ApplyTags applyTags) + { + var currentTags = GetTags(item); + + switch (applyTags) + { + case ApplyTags.Add: + newTags.ForEach(t => currentTags.Add(t)); + break; + case ApplyTags.Remove: + newTags.ForEach(t => currentTags.Remove(t)); + break; + case ApplyTags.Replace: + SetTags(item, new HashSet(newTags)); + break; + } + } + } +} diff --git a/src/Radarr.Api.V3/MediaItems/IEditorResource.cs b/src/Radarr.Api.V3/MediaItems/IEditorResource.cs new file mode 100644 index 0000000000..5a096d0dd1 --- /dev/null +++ b/src/Radarr.Api.V3/MediaItems/IEditorResource.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace Radarr.Api.V3.MediaItems +{ + public interface IEditorResource + { + List Ids { get; } + bool? Monitored { get; set; } + int? QualityProfileId { get; set; } + string RootFolderPath { get; set; } + List Tags { get; set; } + ApplyTags ApplyTags { get; set; } + bool DeleteFiles { get; set; } + } +}