Fixed: Removed unnecessary author data from book endpoint

pull/1702/head
ta264 2 years ago
parent ce58e6ecdb
commit a59706ceb4

@ -8,6 +8,7 @@ import * as commandNames from 'Commands/commandNames';
import { toggleBooksMonitored } from 'Store/Actions/bookActions';
import { clearBookFiles, fetchBookFiles } from 'Store/Actions/bookFileActions';
import { executeCommand } from 'Store/Actions/commandActions';
import { clearEditions, fetchEditions } from 'Store/Actions/editionActions';
import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions';
import createAllAuthorSelector from 'Store/Selectors/createAllAuthorsSelector';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
@ -43,11 +44,12 @@ function createMapStateToProps() {
(state, { titleSlug }) => titleSlug,
selectBookFiles,
(state) => state.books,
(state) => state.editions,
createAllAuthorSelector(),
createCommandsSelector(),
createUISettingsSelector(),
createDimensionsSelector(),
(titleSlug, bookFiles, books, authors, commands, uiSettings, dimensions) => {
(titleSlug, bookFiles, books, editions, authors, commands, uiSettings, dimensions) => {
const book = books.items.find((b) => b.titleSlug === titleSlug);
const author = authors.find((a) => a.id === book.authorId);
const sortedBooks = books.items.filter((b) => b.authorId === book.authorId);
@ -79,8 +81,8 @@ function createMapStateToProps() {
isRefreshingCommand.body.bookId === book.id
);
const isFetching = isBookFilesFetching;
const isPopulated = isBookFilesPopulated;
const isFetching = isBookFilesFetching || editions.isFetching;
const isPopulated = isBookFilesPopulated && editions.isPopulated;
return {
...book,
@ -104,6 +106,8 @@ const mapDispatchToProps = {
executeCommand,
fetchBookFiles,
clearBookFiles,
fetchEditions,
clearEditions,
clearReleases,
cancelFetchReleases,
toggleBooksMonitored
@ -121,7 +125,8 @@ class BookDetailsConnector extends Component {
}
componentDidUpdate(prevProps) {
if (!_.isEqual(getMonitoredEditions(prevProps), getMonitoredEditions(this.props)) ||
if (prevProps.id !== this.props.id ||
!_.isEqual(getMonitoredEditions(prevProps), getMonitoredEditions(this.props)) ||
(prevProps.anyReleaseOk === false && this.props.anyReleaseOk === true)) {
this.unpopulate();
this.populate();
@ -140,12 +145,14 @@ class BookDetailsConnector extends Component {
const bookId = this.props.id;
this.props.fetchBookFiles({ bookId });
this.props.fetchEditions({ bookId });
}
unpopulate = () => {
this.props.cancelFetchReleases();
this.props.clearReleases();
this.props.clearBookFiles();
this.props.clearEditions();
}
//
@ -195,6 +202,8 @@ BookDetailsConnector.propTypes = {
titleSlug: PropTypes.string.isRequired,
fetchBookFiles: PropTypes.func.isRequired,
clearBookFiles: PropTypes.func.isRequired,
fetchEditions: PropTypes.func.isRequired,
clearEditions: PropTypes.func.isRequired,
clearReleases: PropTypes.func.isRequired,
cancelFetchReleases: PropTypes.func.isRequired,
toggleBooksMonitored: PropTypes.func.isRequired,

@ -8,15 +8,25 @@ import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import BookDetailsHeader from './BookDetailsHeader';
const selectOverview = createSelector(
(state) => state.editions,
(editions) => {
const monitored = editions.items.find((e) => e.monitored === true);
return monitored?.overview;
}
);
function createMapStateToProps() {
return createSelector(
createBookSelector(),
selectOverview,
createUISettingsSelector(),
createDimensionsSelector(),
(book, uiSettings, dimensions) => {
(book, overview, uiSettings, dimensions) => {
return {
...book,
overview,
shortDateFormat: uiSettings.shortDateFormat,
isSmallScreen: dimensions.isSmallScreen
};

@ -53,7 +53,7 @@ class EditBookModalContent extends Component {
editions
} = item;
const hasFile = statistics ? statistics.bookFileCount : 0;
const hasFile = statistics ? statistics.bookFileCount > 0 : false;
const errorMessage = getErrorMessage(error, 'Unable to load editions');
return (

@ -4,7 +4,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { saveBook, setBookValue } from 'Store/Actions/bookActions';
import { clearEditions, fetchEditions } from 'Store/Actions/editionActions';
import { saveEditions } from 'Store/Actions/editionActions';
import createAuthorSelector from 'Store/Selectors/createAuthorSelector';
import createBookSelector from 'Store/Selectors/createBookSelector';
import selectSettings from 'Store/Selectors/selectSettings';
@ -26,17 +26,14 @@ function createMapStateToProps() {
const {
isFetching,
isPopulated,
error,
items
error
} = editionState;
book.editions = items;
const bookSettings = _.pick(book, [
'monitored',
'anyEditionOk',
'editions'
'anyEditionOk'
]);
bookSettings.editions = editionState.items;
const settings = selectSettings(bookSettings, pendingChanges, saveError);
@ -58,10 +55,9 @@ function createMapStateToProps() {
}
const mapDispatchToProps = {
dispatchFetchEditions: fetchEditions,
dispatchClearEditions: clearEditions,
dispatchSetBookValue: setBookValue,
dispatchSaveBook: saveBook
dispatchSaveBook: saveBook,
dispatchSaveEditions: saveEditions
};
class EditBookModalContentConnector extends Component {
@ -69,20 +65,12 @@ class EditBookModalContentConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.dispatchFetchEditions({ bookId: this.props.bookId });
}
componentDidUpdate(prevProps, prevState) {
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
this.props.onModalClose();
}
}
componentWillUnmount() {
this.props.dispatchClearEditions();
}
//
// Listeners
@ -94,6 +82,9 @@ class EditBookModalContentConnector extends Component {
this.props.dispatchSaveBook({
id: this.props.bookId
});
this.props.dispatchSaveEditions({
id: this.props.bookId
});
}
//
@ -114,10 +105,9 @@ EditBookModalContentConnector.propTypes = {
bookId: PropTypes.number,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
dispatchFetchEditions: PropTypes.func.isRequired,
dispatchClearEditions: PropTypes.func.isRequired,
dispatchSetBookValue: PropTypes.func.isRequired,
dispatchSaveBook: PropTypes.func.isRequired,
dispatchSaveEditions: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

@ -5,6 +5,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import { executeCommand } from 'Store/Actions/commandActions';
import createBookAuthorSelector from 'Store/Selectors/createBookAuthorSelector';
import createBookQualityProfileSelector from 'Store/Selectors/createBookQualityProfileSelector';
import createBookSelector from 'Store/Selectors/createBookSelector';
import createExecutingCommandsSelector from 'Store/Selectors/createExecutingCommandsSelector';
@ -32,11 +33,13 @@ function selectShowSearchAction() {
function createMapStateToProps() {
return createSelector(
createBookSelector(),
createBookAuthorSelector(),
createBookQualityProfileSelector(),
selectShowSearchAction(),
createExecutingCommandsSelector(),
(
book,
author,
qualityProfile,
showSearchAction,
executingCommands
@ -54,7 +57,7 @@ function createMapStateToProps() {
const isRefreshingBook = executingCommands.some((command) => {
return (
(command.name === commandNames.REFRESH_AUTHOR &&
command.body.authorId === book.author.id) ||
command.body.authorId === book.authorId) ||
(command.name === commandNames.REFRESH_BOOK &&
command.body.bookId === book.id)
);
@ -63,7 +66,7 @@ function createMapStateToProps() {
const isSearchingBook = executingCommands.some((command) => {
return (
(command.name === commandNames.AUTHOR_SEARCH &&
command.body.authorId === book.author.id) ||
command.body.authorId === book.authorId) ||
(command.name === commandNames.BOOK_SEARCH &&
command.body.bookIds.includes(book.id))
);
@ -71,6 +74,7 @@ function createMapStateToProps() {
return {
...book,
author,
qualityProfile,
showSearchAction,
isRefreshingBook,

@ -12,6 +12,7 @@ import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import { icons } from 'Helpers/Props';
import dimensions from 'Styles/Variables/dimensions';
import fonts from 'Styles/Variables/fonts';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import stripHtml from 'Utilities/String/stripHtml';
import translate from 'Utilities/String/translate';
import BookIndexOverviewInfo from './BookIndexOverviewInfo';
@ -42,10 +43,26 @@ class BookIndexOverview extends Component {
this.state = {
isEditAuthorModalOpen: false,
isDeleteAuthorModalOpen: false
isDeleteAuthorModalOpen: false,
overview: ''
};
}
componentDidMount() {
const { id } = this.props;
// Note that this component is lazy loaded by the virtualised view.
// We want to avoid storing overviews for *all* books which is
// why it's not put into the redux store
const promise = createAjaxRequest({
url: `/book/${id}/overview`
}).request;
promise.done((data) => {
this.setState({ overview: data.overview });
});
}
//
// Listeners
@ -84,7 +101,6 @@ class BookIndexOverview extends Component {
const {
id,
title,
overview,
monitored,
titleSlug,
nextAiring,
@ -118,6 +134,7 @@ class BookIndexOverview extends Component {
} = statistics;
const {
overview,
isEditAuthorModalOpen,
isDeleteAuthorModalOpen
} = this.state;
@ -267,7 +284,6 @@ class BookIndexOverview extends Component {
BookIndexOverview.propTypes = {
id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
overview: PropTypes.string.isRequired,
monitored: PropTypes.bool.isRequired,
titleSlug: PropTypes.string.isRequired,
nextAiring: PropTypes.string,

@ -1,5 +1,8 @@
import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions';
import { createThunk, handleThunks } from 'Store/thunks';
import getProviderState from 'Utilities/State/getProviderState';
import { updateItem } from './baseActions';
import createFetchHandler from './Creators/createFetchHandler';
import createHandleActions from './Creators/createHandleActions';
import createClearReducer from './Creators/Reducers/createClearReducer';
@ -25,18 +28,39 @@ export const defaultState = {
export const FETCH_EDITIONS = 'editions/fetchEditions';
export const CLEAR_EDITIONS = 'editions/clearEditions';
export const SAVE_EDITIONS = 'editions/saveEditions';
//
// Action Creators
export const fetchEditions = createThunk(FETCH_EDITIONS);
export const clearEditions = createAction(CLEAR_EDITIONS);
export const saveEditions = createThunk(SAVE_EDITIONS);
//
// Action Handlers
export const actionHandlers = handleThunks({
[FETCH_EDITIONS]: createFetchHandler(section, '/edition')
[FETCH_EDITIONS]: createFetchHandler(section, '/edition'),
[SAVE_EDITIONS]: function(getState, payload, dispatch) {
const {
id,
...otherPayload
} = payload;
const saveData = getProviderState({ id, ...otherPayload }, getState, 'books');
dispatch(batchActions([
...saveData.editions.map((edition) => {
return updateItem({
id: edition.id,
section: 'editions',
...edition
});
})
]));
}
});
//

@ -0,0 +1,15 @@
import { createSelector } from 'reselect';
import createBookSelector from './createBookSelector';
function createBookAuthorSelector() {
return createSelector(
createBookSelector(),
(state) => state.authors.itemMap,
(state) => state.authors.items,
(book, authorMap, allAuthors) => {
return allAuthors[authorMap[book.authorId]];
}
);
}
export default createBookAuthorSelector;

@ -1,18 +1,16 @@
import { createSelector } from 'reselect';
import createBookSelector from './createBookSelector';
import createBookAuthorSelector from './createBookAuthorSelector';
function createBookQualityProfileSelector() {
return createSelector(
(state) => state.settings.qualityProfiles.items,
createBookSelector(),
(qualityProfiles, book) => {
if (!book) {
createBookAuthorSelector(),
(qualityProfiles, author) => {
if (!author) {
return {};
}
return qualityProfiles.find((profile) => {
return profile.id === book.author.qualityProfileId;
});
return qualityProfiles.find((profile) => profile.id === author.qualityProfileId);
}
);
}

@ -439,7 +439,7 @@ namespace NzbDrone.Core.MetadataSource.BookInfo
var edition = book.Editions.Value.SingleOrDefault(e => e.ForeignEditionId == id.ToString());
trimmed.Editions = new List<Edition> { edition };
return trimmed;
book = trimmed;
}
var authorDict = authors.ToDictionary(x => x.ForeignAuthorId);

@ -52,12 +52,23 @@ namespace NzbDrone.Integration.Test.ApiTests
{
EnsureAuthor("14586394", "43765115", "Andrew Hunter Murray", true);
var result = WantedMissing.GetPaged(0, 15, "releaseDate", "desc");
var result = WantedMissing.GetPagedIncludeAuthor(0, 15, "releaseDate", "desc", includeAuthor: true);
result.Records.First().Author.Should().NotBeNull();
result.Records.First().Author.AuthorName.Should().Be("Andrew Hunter Murray");
}
[Test]
[Order(1)]
public void missing_should_not_have_author()
{
EnsureAuthor("14586394", "43765115", "Andrew Hunter Murray", true);
var result = WantedMissing.GetPagedIncludeAuthor(0, 15, "releaseDate", "desc", includeAuthor: false);
result.Records.First().Author.Should().BeNull();
}
[Test]
[Order(2)]
public void cutoff_should_have_monitored_items()
@ -103,12 +114,25 @@ namespace NzbDrone.Integration.Test.ApiTests
var author = EnsureAuthor("14586394", "43765115", "Andrew Hunter Murray", true);
EnsureBookFile(author, 1, "43765115", Quality.MOBI);
var result = WantedCutoffUnmet.GetPaged(0, 15, "releaseDate", "desc");
var result = WantedCutoffUnmet.GetPagedIncludeAuthor(0, 15, "releaseDate", "desc", includeAuthor: true);
result.Records.First().Author.Should().NotBeNull();
result.Records.First().Author.AuthorName.Should().Be("Andrew Hunter Murray");
}
[Test]
[Order(2)]
public void cutoff_should_not_have_author()
{
EnsureProfileCutoff(1, Quality.AZW3);
var author = EnsureAuthor("14586394", "43765115", "Andrew Hunter Murray", true);
EnsureBookFile(author, 1, "43765115", Quality.MOBI);
var result = WantedCutoffUnmet.GetPagedIncludeAuthor(0, 15, "releaseDate", "desc", includeAuthor: false);
result.Records.First().Author.Should().BeNull();
}
[Test]
[Order(1)]
public void missing_should_have_unmonitored_items()

@ -0,0 +1,40 @@
using System.Collections.Generic;
using Readarr.Api.V1.Books;
using Readarr.Http;
using RestSharp;
namespace NzbDrone.Integration.Test.Client
{
public class WantedClient : ClientBase<BookResource>
{
public WantedClient(IRestClient restClient, string apiKey, string resource)
: base(restClient, apiKey, resource)
{
}
public PagingResource<BookResource> GetPagedIncludeAuthor(int pageNumber, int pageSize, string sortKey, string sortDir, string filterKey = null, string filterValue = null, bool includeAuthor = true)
{
var request = BuildRequest();
request.AddParameter("page", pageNumber);
request.AddParameter("pageSize", pageSize);
request.AddParameter("sortKey", sortKey);
request.AddParameter("sortDir", sortDir);
if (filterKey != null && filterValue != null)
{
request.AddParameter("filterKey", filterKey);
request.AddParameter("filterValue", filterValue);
}
request.AddParameter("includeAuthor", includeAuthor);
return Get<PagingResource<BookResource>>(request);
}
public List<BookResource> GetBooksInAuthor(int authorId)
{
var request = BuildRequest("?authorId=" + authorId.ToString());
return Get<List<BookResource>>(request);
}
}
}

@ -53,8 +53,8 @@ namespace NzbDrone.Integration.Test
public ClientBase<RootFolderResource> RootFolders;
public AuthorClient Author;
public ClientBase<TagResource> Tags;
public ClientBase<BookResource> WantedMissing;
public ClientBase<BookResource> WantedCutoffUnmet;
public WantedClient WantedMissing;
public WantedClient WantedCutoffUnmet;
private List<SignalRMessage> _signalRReceived;
@ -118,8 +118,8 @@ namespace NzbDrone.Integration.Test
RootFolders = new ClientBase<RootFolderResource>(RestClient, ApiKey);
Author = new AuthorClient(RestClient, ApiKey);
Tags = new ClientBase<TagResource>(RestClient, ApiKey);
WantedMissing = new ClientBase<BookResource>(RestClient, ApiKey, "wanted/missing");
WantedCutoffUnmet = new ClientBase<BookResource>(RestClient, ApiKey, "wanted/cutoff");
WantedMissing = new WantedClient(RestClient, ApiKey, "wanted/missing");
WantedCutoffUnmet = new WantedClient(RestClient, ApiKey, "wanted/cutoff");
}
[OneTimeTearDown]

@ -138,6 +138,17 @@ namespace Readarr.Api.V1.Books
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<BookResource> AddBook(BookResource bookResource)
{

@ -72,13 +72,10 @@ namespace Readarr.Api.V1.Books
AuthorTitle = authorTitle,
SeriesTitle = seriesTitle,
Disambiguation = selectedEdition?.Disambiguation,
Overview = selectedEdition?.Overview,
Images = selectedEdition?.Images ?? new List<MediaCover>(),
Links = model.Links.Concat(selectedEdition?.Links ?? new List<Links>()).ToList(),
Ratings = selectedEdition?.Ratings ?? new Ratings(),
Added = model.Added,
Author = model.Author?.Value.ToResource(),
Editions = model.Editions?.Value.ToResource() ?? new List<EditionResource>()
};
}

@ -51,6 +51,9 @@ namespace Readarr.Api.V1.Search
{
var book = (NzbDrone.Core.Books.Book)result;
resource.Book = book.ToResource();
resource.Book.Overview = book.Editions.Value.Single(x => x.Monitored).Overview;
resource.Book.Author = book.Author.Value.ToResource();
resource.Book.Editions = book.Editions.Value.ToResource();
resource.ForeignId = book.ForeignBookId;
var cover = book.Editions.Value.Single(x => x.Monitored).Images.FirstOrDefault(c => c.CoverType == MediaCoverTypes.Cover);

Loading…
Cancel
Save