using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using FluentValidation; using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; using NzbDrone.Core.AuthorStats; using NzbDrone.Core.Books; using NzbDrone.Core.Books.Events; using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Download; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Validation; using NzbDrone.Core.Validation.Paths; using NzbDrone.Http.REST.Attributes; using NzbDrone.SignalR; using Readarr.Http; using Readarr.Http.Extensions; namespace Readarr.Api.V1.Books { [V1ApiController] public class BookController : BookControllerWithSignalR, IHandle, IHandle, IHandle, IHandle, IHandle, IHandle, IHandle { protected readonly IAuthorService _authorService; protected readonly IEditionService _editionService; protected readonly IAddBookService _addBookService; public BookController(IAuthorService authorService, IBookService bookService, IAddBookService addBookService, IEditionService editionService, ISeriesBookLinkService seriesBookLinkService, IAuthorStatisticsService authorStatisticsService, IMapCoversToLocal coverMapper, IUpgradableSpecification upgradableSpecification, IBroadcastSignalRMessage signalRBroadcaster, QualityProfileExistsValidator qualityProfileExistsValidator, MetadataProfileExistsValidator metadataProfileExistsValidator) : base(bookService, seriesBookLinkService, authorStatisticsService, coverMapper, upgradableSpecification, signalRBroadcaster) { _authorService = authorService; _editionService = editionService; _addBookService = addBookService; PostValidator.RuleFor(s => s.ForeignBookId).NotEmpty(); PostValidator.RuleFor(s => s.Author.QualityProfileId).SetValidator(qualityProfileExistsValidator); PostValidator.RuleFor(s => s.Author.MetadataProfileId).SetValidator(metadataProfileExistsValidator); PostValidator.RuleFor(s => s.Author.RootFolderPath).IsValidPath().When(s => s.Author.Path.IsNullOrWhiteSpace()); PostValidator.RuleFor(s => s.Author.ForeignAuthorId).NotEmpty(); } [HttpGet] public List GetBooks([FromQuery]int? authorId, [FromQuery]List bookIds, [FromQuery]string titleSlug, [FromQuery]bool includeAllAuthorBooks = false) { if (!authorId.HasValue && !bookIds.Any() && titleSlug.IsNullOrWhiteSpace()) { var editionTask = Task.Run(() => _editionService.GetAllMonitoredEditions()); var metadataTask = Task.Run(() => _authorService.GetAllAuthors()); var books = _bookService.GetAllBooks(); var editions = editionTask.GetAwaiter().GetResult().GroupBy(x => x.BookId).ToDictionary(x => x.Key, y => y.ToList()); var authors = metadataTask.GetAwaiter().GetResult().ToDictionary(x => x.AuthorMetadataId); foreach (var book in books) { book.Author = authors[book.AuthorMetadataId]; if (editions.TryGetValue(book.Id, out var bookEditions)) { book.Editions = bookEditions; } else { book.Editions = new List(); } } return MapToResource(books, false); } if (authorId.HasValue) { var books = _bookService.GetBooksByAuthor(authorId.Value); var author = _authorService.GetAuthor(authorId.Value); var editions = _editionService.GetEditionsByAuthor(authorId.Value) .GroupBy(x => x.BookId) .ToDictionary(x => x.Key, y => y.ToList()); foreach (var book in books) { book.Author = author; if (editions.TryGetValue(book.Id, out var bookEditions)) { book.Editions = bookEditions; } else { book.Editions = new List(); } } return MapToResource(books, false); } if (titleSlug.IsNotNullOrWhiteSpace()) { var book = _bookService.FindBySlug(titleSlug); if (book == null) { return MapToResource(new List(), false); } if (includeAllAuthorBooks) { return MapToResource(_bookService.GetBooksByAuthor(book.AuthorId), false); } else { return MapToResource(new List { book }, false); } } return MapToResource(_bookService.GetBooks(bookIds), false); } [HttpGet("{id:int}/overview")] public object Overview(int id) { var overview = _editionService.GetEditionsByBook(id).Single(x => x.Monitored).Overview; return new { id, overview }; } [RestPostById] public ActionResult AddBook(BookResource bookResource) { var book = _addBookService.AddBook(bookResource.ToModel()); return Created(book.Id); } [RestPutById] public ActionResult UpdateBook(BookResource bookResource) { var book = _bookService.GetBook(bookResource.Id); var model = bookResource.ToModel(book); _bookService.UpdateBook(model); _editionService.UpdateMany(model.Editions.Value); BroadcastResourceChange(ModelAction.Updated, model.Id); return Accepted(model.Id); } [RestDeleteById] public void DeleteBook(int id) { var deleteFiles = Request.GetBooleanQueryParameter("deleteFiles"); var addImportListExclusion = Request.GetBooleanQueryParameter("addImportListExclusion"); _bookService.DeleteBook(id, deleteFiles, addImportListExclusion); } [HttpPut("monitor")] public IActionResult SetBooksMonitored([FromBody]BooksMonitoredResource resource) { _bookService.SetMonitored(resource.BookIds, resource.Monitored); if (resource.BookIds.Count == 1) { _bookService.SetBookMonitored(resource.BookIds.First(), resource.Monitored); } else { _bookService.SetMonitored(resource.BookIds, resource.Monitored); } return Accepted(MapToResource(_bookService.GetBooks(resource.BookIds), false)); } [NonAction] public void Handle(BookGrabbedEvent message) { foreach (var book in message.Book.Books) { var resource = book.ToResource(); resource.Grabbed = true; BroadcastResourceChange(ModelAction.Updated, resource); } } [NonAction] public void Handle(BookEditedEvent message) { BroadcastResourceChange(ModelAction.Updated, MapToResource(message.Book, true)); } [NonAction] public void Handle(BookUpdatedEvent message) { BroadcastResourceChange(ModelAction.Updated, MapToResource(message.Book, true)); } [NonAction] public void Handle(BookDeletedEvent message) { BroadcastResourceChange(ModelAction.Deleted, message.Book.ToResource()); } [NonAction] public void Handle(BookImportedEvent message) { BroadcastResourceChange(ModelAction.Updated, MapToResource(message.Book, true)); } [NonAction] public void Handle(TrackImportedEvent message) { BroadcastResourceChange(ModelAction.Updated, message.BookInfo.Book.ToResource()); } [NonAction] public void Handle(BookFileDeletedEvent message) { if (message.Reason == DeleteMediaFileReason.Upgrade) { return; } BroadcastResourceChange(ModelAction.Updated, MapToResource(message.BookFile.Edition.Value.Book.Value, true)); } } }