Fixed: Faster artist endpoint (#874)

* Fixed: Speed up AllArtist API endpoint

* New: Display UI before artists have loaded

* Add test of new repository methods
pull/6/head
ta264 5 years ago committed by GitHub
parent 698d5e1cf5
commit 0352f8d3ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -50,6 +50,10 @@ class BlacklistRow extends Component {
onRemovePress onRemovePress
} = this.props; } = this.props;
if (!artist) {
return null;
}
return ( return (
<TableRow> <TableRow>
{ {

@ -67,7 +67,7 @@ class HistoryRow extends Component {
onMarkAsFailedPress onMarkAsFailedPress
} = this.props; } = this.props;
if (!album) { if (!artist || !album) {
return null; return null;
} }

@ -1,5 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import getSelectedIds from 'Utilities/Table/getSelectedIds'; import getSelectedIds from 'Utilities/Table/getSelectedIds';
import selectAll from 'Utilities/Table/selectAll'; import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected'; import toggleSelected from 'Utilities/Table/toggleSelected';
@ -145,7 +146,7 @@ class AlbumStudio extends Component {
{ {
!isFetching && !!error && !isFetching && !!error &&
<div>Unable to load the Album Studio</div> <div>{getErrorMessage(error, 'Failed to load artist from API')}</div>
} }
{ {

@ -1,5 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import getSelectedIds from 'Utilities/Table/getSelectedIds'; import getSelectedIds from 'Utilities/Table/getSelectedIds';
import selectAll from 'Utilities/Table/selectAll'; import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected'; import toggleSelected from 'Utilities/Table/toggleSelected';
@ -209,7 +210,7 @@ class ArtistEditor extends Component {
{ {
!isFetching && !!error && !isFetching && !!error &&
<div>Unable to load the calendar</div> <div>{getErrorMessage(error, 'Failed to load artist from API')}</div>
} }
{ {

@ -4,6 +4,12 @@
overflow: hidden; overflow: hidden;
} }
.errorMessage {
margin-top: 20px;
text-align: center;
font-size: 20px;
}
.contentBody { .contentBody {
composes: contentBody from '~Components/Page/PageContentBody.css'; composes: contentBody from '~Components/Page/PageContentBody.css';

@ -2,6 +2,7 @@ import _ from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import { align, icons, sortDirections } from 'Helpers/Props'; import { align, icons, sortDirections } from 'Helpers/Props';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent'; import PageContent from 'Components/Page/PageContent';
@ -340,7 +341,9 @@ class ArtistIndex extends Component {
{ {
!isFetching && !!error && !isFetching && !!error &&
<div>Unable to load artist</div> <div className={styles.errorMessage}>
{getErrorMessage(error, 'Failed to load artist from API')}
</div>
} }
{ {

@ -12,3 +12,9 @@
flex-grow: 1; flex-grow: 1;
width: 100%; width: 100%;
} }
.errorMessage {
margin-top: 20px;
text-align: center;
font-size: 20px;
}

@ -1,5 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import { align, icons } from 'Helpers/Props'; import { align, icons } from 'Helpers/Props';
import PageContent from 'Components/Page/PageContent'; import PageContent from 'Components/Page/PageContent';
import Measure from 'Components/Measure'; import Measure from 'Components/Measure';
@ -75,6 +76,7 @@ class CalendarPage extends Component {
selectedFilterKey, selectedFilterKey,
filters, filters,
hasArtist, hasArtist,
artistError,
missingAlbumIds, missingAlbumIds,
isSearchingForMissing, isSearchingForMissing,
useCurrentPage, useCurrentPage,
@ -131,6 +133,15 @@ class CalendarPage extends Component {
className={styles.calendarPageBody} className={styles.calendarPageBody}
innerClassName={styles.calendarInnerPageBody} innerClassName={styles.calendarInnerPageBody}
> >
{
artistError &&
<div className={styles.errorMessage}>
{getErrorMessage(artistError, 'Failed to load artist from API')}
</div>
}
{
!artistError &&
<Measure <Measure
whitelist={['width']} whitelist={['width']}
onMeasure={this.onMeasure} onMeasure={this.onMeasure}
@ -143,9 +154,10 @@ class CalendarPage extends Component {
<div /> <div />
} }
</Measure> </Measure>
}
{ {
hasArtist && hasArtist && !!artistError &&
<LegendConnector /> <LegendConnector />
} }
</PageContentBodyConnector> </PageContentBodyConnector>
@ -169,6 +181,7 @@ CalendarPage.propTypes = {
selectedFilterKey: PropTypes.string.isRequired, selectedFilterKey: PropTypes.string.isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired, filters: PropTypes.arrayOf(PropTypes.object).isRequired,
hasArtist: PropTypes.bool.isRequired, hasArtist: PropTypes.bool.isRequired,
artistError: PropTypes.object,
missingAlbumIds: PropTypes.arrayOf(PropTypes.number).isRequired, missingAlbumIds: PropTypes.arrayOf(PropTypes.number).isRequired,
isSearchingForMissing: PropTypes.bool.isRequired, isSearchingForMissing: PropTypes.bool.isRequired,
useCurrentPage: PropTypes.bool.isRequired, useCurrentPage: PropTypes.bool.isRequired,

@ -72,7 +72,8 @@ function createMapStateToProps() {
selectedFilterKey, selectedFilterKey,
filters, filters,
colorImpairedMode: uiSettings.enableColorImpairedMode, colorImpairedMode: uiSettings.enableColorImpairedMode,
hasArtist: !!artistCount, hasArtist: !!artistCount.count,
artistError: artistCount.error,
missingAlbumIds, missingAlbumIds,
isSearchingForMissing isSearchingForMissing
}; };

@ -43,7 +43,6 @@ const selectAppProps = createSelector(
); );
const selectIsPopulated = createSelector( const selectIsPopulated = createSelector(
(state) => state.artist.isPopulated,
(state) => state.customFilters.isPopulated, (state) => state.customFilters.isPopulated,
(state) => state.tags.isPopulated, (state) => state.tags.isPopulated,
(state) => state.settings.ui.isPopulated, (state) => state.settings.ui.isPopulated,
@ -52,7 +51,6 @@ const selectIsPopulated = createSelector(
(state) => state.settings.importLists.isPopulated, (state) => state.settings.importLists.isPopulated,
(state) => state.system.status.isPopulated, (state) => state.system.status.isPopulated,
( (
artistIsPopulated,
customFiltersIsPopulated, customFiltersIsPopulated,
tagsIsPopulated, tagsIsPopulated,
uiSettingsIsPopulated, uiSettingsIsPopulated,
@ -62,7 +60,6 @@ const selectIsPopulated = createSelector(
systemStatusIsPopulated systemStatusIsPopulated
) => { ) => {
return ( return (
artistIsPopulated &&
customFiltersIsPopulated && customFiltersIsPopulated &&
tagsIsPopulated && tagsIsPopulated &&
uiSettingsIsPopulated && uiSettingsIsPopulated &&
@ -75,7 +72,6 @@ const selectIsPopulated = createSelector(
); );
const selectErrors = createSelector( const selectErrors = createSelector(
(state) => state.artist.error,
(state) => state.customFilters.error, (state) => state.customFilters.error,
(state) => state.tags.error, (state) => state.tags.error,
(state) => state.settings.ui.error, (state) => state.settings.ui.error,
@ -84,7 +80,6 @@ const selectErrors = createSelector(
(state) => state.settings.importLists.error, (state) => state.settings.importLists.error,
(state) => state.system.status.error, (state) => state.system.status.error,
( (
artistError,
customFiltersError, customFiltersError,
tagsError, tagsError,
uiSettingsError, uiSettingsError,
@ -94,7 +89,6 @@ const selectErrors = createSelector(
systemStatusError systemStatusError
) => { ) => {
const hasError = !!( const hasError = !!(
artistError ||
customFiltersError || customFiltersError ||
tagsError || tagsError ||
uiSettingsError || uiSettingsError ||
@ -106,7 +100,6 @@ const selectErrors = createSelector(
return { return {
hasError, hasError,
artistError,
customFiltersError, customFiltersError,
tagsError, tagsError,
uiSettingsError, uiSettingsError,

@ -4,8 +4,12 @@ import createAllArtistSelector from './createAllArtistSelector';
function createArtistCountSelector() { function createArtistCountSelector() {
return createSelector( return createSelector(
createAllArtistSelector(), createAllArtistSelector(),
(artists) => { (state) => state.artist.error,
return artists.length; (artists, error) => {
return {
count: artists.length,
error
};
} }
); );
} }

@ -26,6 +26,10 @@ function CutoffUnmetRow(props) {
onSelectedChange onSelectedChange
} = props; } = props;
if (!artist) {
return null;
}
return ( return (
<TableRow> <TableRow>
<TableSelectCell <TableSelectCell

@ -184,11 +184,13 @@ namespace Lidarr.Api.V1.Artist
private void LinkNextPreviousAlbums(params ArtistResource[] artists) private void LinkNextPreviousAlbums(params ArtistResource[] artists)
{ {
var nextAlbums = _albumService.GetNextAlbumsByArtistMetadataId(artists.Select(x => x.ArtistMetadataId));
var lastAlbums = _albumService.GetLastAlbumsByArtistMetadataId(artists.Select(x => x.ArtistMetadataId));
foreach (var artistResource in artists) foreach (var artistResource in artists)
{ {
var artistAlbums = _albumService.GetAlbumsByArtist(artistResource.Id).OrderBy(s=>s.ReleaseDate); artistResource.NextAlbum = nextAlbums.FirstOrDefault(x => x.ArtistMetadataId == artistResource.ArtistMetadataId);
artistResource.NextAlbum = artistAlbums.Where(s => s.ReleaseDate >= DateTime.UtcNow && s.Monitored).FirstOrDefault(); artistResource.LastAlbum = lastAlbums.FirstOrDefault(x => x.ArtistMetadataId == artistResource.ArtistMetadataId);
artistResource.LastAlbum = artistAlbums.Where(s => s.ReleaseDate <= DateTime.UtcNow && s.Monitored).LastOrDefault();
} }
} }

@ -5,6 +5,7 @@ using NzbDrone.Common.Extensions;
using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaCover;
using NzbDrone.Core.Music; using NzbDrone.Core.Music;
using Lidarr.Http.REST; using Lidarr.Http.REST;
using Newtonsoft.Json;
namespace Lidarr.Api.V1.Artist namespace Lidarr.Api.V1.Artist
{ {
@ -14,6 +15,8 @@ namespace Lidarr.Api.V1.Artist
//Todo: Is there an easy way to keep IgnoreArticlesWhenSorting in sync between, Series, History, Missing? //Todo: Is there an easy way to keep IgnoreArticlesWhenSorting in sync between, Series, History, Missing?
//Todo: We should get the entire Profile instead of ID and Name separately //Todo: We should get the entire Profile instead of ID and Name separately
[JsonIgnore]
public int ArtistMetadataId { get; set; }
public ArtistStatusType Status { get; set; } public ArtistStatusType Status { get; set; }
public bool Ended => Status == ArtistStatusType.Ended; public bool Ended => Status == ArtistStatusType.Ended;
@ -70,6 +73,7 @@ namespace Lidarr.Api.V1.Artist
return new ArtistResource return new ArtistResource
{ {
Id = model.Id, Id = model.Id,
ArtistMetadataId = model.ArtistMetadataId,
ArtistName = model.Name, ArtistName = model.Name,
//AlternateTitles //AlternateTitles

@ -3,7 +3,9 @@ using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Core.Music; using NzbDrone.Core.Music;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
namespace NzbDrone.Core.Test.MusicTests.AlbumRepositoryTests namespace NzbDrone.Core.Test.MusicTests.AlbumRepositoryTests
{ {
@ -13,6 +15,7 @@ namespace NzbDrone.Core.Test.MusicTests.AlbumRepositoryTests
private Artist _artist; private Artist _artist;
private Album _album; private Album _album;
private Album _albumSpecial; private Album _albumSpecial;
private List<Album> _albums;
private AlbumRelease _release; private AlbumRelease _release;
private AlbumRepository _albumRepo; private AlbumRepository _albumRepo;
private ReleaseRepository _releaseRepo; private ReleaseRepository _releaseRepo;
@ -150,5 +153,47 @@ namespace NzbDrone.Core.Test.MusicTests.AlbumRepositoryTests
album.Should().BeNull(); album.Should().BeNull();
} }
private void GivenMultipleAlbums()
{
_albums = Builder<Album>.CreateListOfSize(4)
.All()
.With(x => x.Id = 0)
.With(x => x.Artist = _artist)
.With(x => x.ArtistMetadataId = _artist.ArtistMetadataId)
.TheFirst(1)
// next
.With(x => x.ReleaseDate = DateTime.UtcNow.AddDays(1))
.TheNext(1)
// another future one
.With(x => x.ReleaseDate = DateTime.UtcNow.AddDays(2))
.TheNext(1)
// most recent
.With(x => x.ReleaseDate = DateTime.UtcNow.AddDays(-1))
.TheNext(1)
// an older one
.With(x => x.ReleaseDate = DateTime.UtcNow.AddDays(-2))
.BuildList();
_albumRepo.InsertMany(_albums);
}
[Test]
public void get_next_albums_should_return_next_album()
{
GivenMultipleAlbums();
var result = _albumRepo.GetNextAlbums(new [] { _artist.ArtistMetadataId });
result.Should().BeEquivalentTo(_albums.Take(1));
}
[Test]
public void get_last_albums_should_return_next_album()
{
GivenMultipleAlbums();
var result = _albumRepo.GetLastAlbums(new [] { _artist.ArtistMetadataId });
result.Should().BeEquivalentTo(_albums.Skip(2).Take(1));
}
} }
} }

@ -58,7 +58,7 @@ namespace NzbDrone.Core.Datastore
public IEnumerable<TModel> All() public IEnumerable<TModel> All()
{ {
return DataMapper.Query<TModel>().ToList(); return Query.ToList();
} }
public int Count() public int Count()

@ -14,6 +14,8 @@ namespace NzbDrone.Core.Music
public interface IAlbumRepository : IBasicRepository<Album> public interface IAlbumRepository : IBasicRepository<Album>
{ {
List<Album> GetAlbums(int artistId); List<Album> GetAlbums(int artistId);
List<Album> GetLastAlbums(IEnumerable<int> artistMetadataIds);
List<Album> GetNextAlbums(IEnumerable<int> artistMetadataIds);
List<Album> GetAlbumsByArtistMetadataId(int artistMetadataId); List<Album> GetAlbumsByArtistMetadataId(int artistMetadataId);
List<Album> GetAlbumsForRefresh(int artistId, IEnumerable<string> foreignIds); List<Album> GetAlbumsForRefresh(int artistId, IEnumerable<string> foreignIds);
Album FindByTitle(int artistMetadataId, string title); Album FindByTitle(int artistMetadataId, string title);
@ -47,6 +49,32 @@ namespace NzbDrone.Core.Music
.Where<Artist>(a => a.Id == artistId).ToList(); .Where<Artist>(a => a.Id == artistId).ToList();
} }
public List<Album> GetLastAlbums(IEnumerable<int> artistMetadataIds)
{
string query = string.Format("SELECT Albums.* " +
"FROM Albums " +
"WHERE Albums.ArtistMetadataId IN ({0}) " +
"AND Albums.ReleaseDate < datetime('now') " +
"GROUP BY Albums.ArtistMetadataId " +
"HAVING Albums.ReleaseDate = MAX(Albums.ReleaseDate)",
string.Join(", ", artistMetadataIds));
return Query.QueryText(query);
}
public List<Album> GetNextAlbums(IEnumerable<int> artistMetadataIds)
{
string query = string.Format("SELECT Albums.* " +
"FROM Albums " +
"WHERE Albums.ArtistMetadataId IN ({0}) " +
"AND Albums.ReleaseDate > datetime('now') " +
"GROUP BY Albums.ArtistMetadataId " +
"HAVING Albums.ReleaseDate = MIN(Albums.ReleaseDate)",
string.Join(", ", artistMetadataIds));
return Query.QueryText(query);
}
public List<Album> GetAlbumsByArtistMetadataId(int artistMetadataId) public List<Album> GetAlbumsByArtistMetadataId(int artistMetadataId)
{ {
return Query.Where(s => s.ArtistMetadataId == artistMetadataId); return Query.Where(s => s.ArtistMetadataId == artistMetadataId);

@ -15,6 +15,8 @@ namespace NzbDrone.Core.Music
Album GetAlbum(int albumId); Album GetAlbum(int albumId);
List<Album> GetAlbums(IEnumerable<int> albumIds); List<Album> GetAlbums(IEnumerable<int> albumIds);
List<Album> GetAlbumsByArtist(int artistId); List<Album> GetAlbumsByArtist(int artistId);
List<Album> GetNextAlbumsByArtistMetadataId(IEnumerable<int> artistMetadataIds);
List<Album> GetLastAlbumsByArtistMetadataId(IEnumerable<int> artistMetadataIds);
List<Album> GetAlbumsByArtistMetadataId(int artistMetadataId); List<Album> GetAlbumsByArtistMetadataId(int artistMetadataId);
List<Album> GetAlbumsForRefresh(int artistMetadataId, IEnumerable<string> foreignIds); List<Album> GetAlbumsForRefresh(int artistMetadataId, IEnumerable<string> foreignIds);
Album AddAlbum(Album newAlbum); Album AddAlbum(Album newAlbum);
@ -170,6 +172,16 @@ namespace NzbDrone.Core.Music
return _albumRepository.GetAlbums(artistId).ToList(); return _albumRepository.GetAlbums(artistId).ToList();
} }
public List<Album> GetNextAlbumsByArtistMetadataId(IEnumerable<int> artistMetadataIds)
{
return _albumRepository.GetNextAlbums(artistMetadataIds).ToList();
}
public List<Album> GetLastAlbumsByArtistMetadataId(IEnumerable<int> artistMetadataIds)
{
return _albumRepository.GetLastAlbums(artistMetadataIds).ToList();
}
public List<Album> GetAlbumsByArtistMetadataId(int artistMetadataId) public List<Album> GetAlbumsByArtistMetadataId(int artistMetadataId)
{ {
return _albumRepository.GetAlbumsByArtistMetadataId(artistMetadataId).ToList(); return _albumRepository.GetAlbumsByArtistMetadataId(artistMetadataId).ToList();

Loading…
Cancel
Save