refactor: extract BaseMediaEditorController for bulk operations (#140)

- 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 <admin@ardentleatherworks.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Cody Kickertz 2025-12-23 09:14:18 -06:00 committed by GitHub
parent 2b42b33d3a
commit cfa42cbfba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 164 additions and 148 deletions

View file

@ -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<Audiobook, AudiobookResource, AudiobookEditorResource>
{
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<Audiobook> GetItemsByIds(List<int> ids) => _audiobookService.GetAudiobooks(ids);
protected override List<Audiobook> UpdateItems(List<Audiobook> items) => _audiobookService.UpdateAudiobooks(items);
protected override void DeleteItems(List<int> 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<int>(newTags);
break;
}
}
var validationResult = _audiobookEditorValidator.Validate(audiobook);
if (!validationResult.IsValid)
{
throw new ValidationException(validationResult.Errors);
}
}
var updatedAudiobooks = _audiobookService.UpdateAudiobooks(audiobooksToUpdate);
var audiobooksResources = new List<AudiobookResource>(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<int> GetTags(Audiobook item) => item.Tags;
protected override void SetTags(Audiobook item, HashSet<int> tags) => item.Tags = tags;
}
}

View file

@ -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<int> 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<int> IEditorResource.Ids => AudiobookIds;
}
}

View file

@ -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<Book, BookResource, BookEditorResource>
{
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<Book> GetItemsByIds(List<int> ids) => _bookService.GetBooks(ids);
protected override List<Book> UpdateItems(List<Book> items) => _bookService.UpdateBooks(items);
protected override void DeleteItems(List<int> 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<int>(newTags);
break;
}
}
var validationResult = _bookEditorValidator.Validate(book);
if (!validationResult.IsValid)
{
throw new ValidationException(validationResult.Errors);
}
}
var updatedBooks = _bookService.UpdateBooks(booksToUpdate);
var booksResources = new List<BookResource>(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<int> GetTags(Book item) => item.Tags;
protected override void SetTags(Book item, HashSet<int> tags) => item.Tags = tags;
}
}

View file

@ -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<int> 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<int> IEditorResource.Ids => BookIds;
}
}

View file

@ -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<TModel, TResource, TEditorResource> : Controller
where TModel : ModelBase, new()
where TResource : RestResource, new()
where TEditorResource : class, IEditorResource
{
protected abstract List<TModel> GetItemsByIds(List<int> ids);
protected abstract List<TModel> UpdateItems(List<TModel> items);
protected abstract void DeleteItems(List<int> 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<int> GetTags(TModel item);
protected abstract void SetTags(TModel item, HashSet<int> 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<TResource>(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<int> 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<int>(newTags));
break;
}
}
}
}

View file

@ -0,0 +1,15 @@
using System.Collections.Generic;
namespace Radarr.Api.V3.MediaItems
{
public interface IEditorResource
{
List<int> Ids { get; }
bool? Monitored { get; set; }
int? QualityProfileId { get; set; }
string RootFolderPath { get; set; }
List<int> Tags { get; set; }
ApplyTags ApplyTags { get; set; }
bool DeleteFiles { get; set; }
}
}