From 7f8dc3d2b434e58a94d2d40048416c13c42f2593 Mon Sep 17 00:00:00 2001 From: ta264 Date: Wed, 21 Jul 2021 21:50:17 +0100 Subject: [PATCH] New: Optionally display authors as LastName, FirstName in index Fixes #1062 --- frontend/src/Author/Index/AuthorIndex.js | 4 +- .../Author/Index/Menus/AuthorIndexSortMenu.js | 6 +-- .../Index/Overview/AuthorIndexOverview.js | 6 ++- .../Index/Overview/AuthorIndexOverviews.js | 5 ++- .../AuthorIndexOverviewOptionsModalContent.js | 27 +++++++++++ .../Author/Index/Posters/AuthorIndexPoster.js | 8 ++-- .../Index/Posters/AuthorIndexPosters.js | 7 +-- .../AuthorIndexPosterOptionsModalContent.js | 11 ++++- .../src/Author/Index/Table/AuthorIndexRow.js | 8 +++- .../Author/Index/Table/AuthorIndexTable.js | 10 +++-- .../Index/Table/AuthorIndexTableConnector.js | 1 + .../Index/Table/AuthorIndexTableOptions.js | 35 ++++++++++++--- .../src/Store/Actions/authorIndexActions.js | 17 +++---- ...AuthorClientSideCollectionItemsSelector.js | 6 ++- .../Array/getIndexOfFirstCharacter.js | 4 +- .../StringExtensionTests/ToSortNameFixture.cs | 2 +- .../Extensions/StringExtensions.cs | 2 +- .../Books/Model/AuthorMetadata.cs | 4 ++ .../Books/Services/AuthorService.cs | 4 +- .../Migration/009_update_author_sort_name.cs | 2 +- .../013_update_author_sort_name_again.cs | 45 +++++++++++++++++++ src/NzbDrone.Core/Localization/Core/en.json | 4 ++ .../Goodreads/GoodreadsProxy.cs | 12 +++-- .../MetadataSource/SkyHook/SkyHookProxy.cs | 4 +- .../Organizer/FileNameBuilder.cs | 2 +- src/NzbDrone.Core/Parser/Parser.cs | 2 +- src/Readarr.Api.V1/Author/AuthorResource.cs | 7 +++ src/Readarr.Api.V1/Queue/QueueController.cs | 2 + 28 files changed, 193 insertions(+), 54 deletions(-) create mode 100644 src/NzbDrone.Core/Datastore/Migration/013_update_author_sort_name_again.cs diff --git a/frontend/src/Author/Index/AuthorIndex.js b/frontend/src/Author/Index/AuthorIndex.js index 3d89c4872..218486540 100644 --- a/frontend/src/Author/Index/AuthorIndex.js +++ b/frontend/src/Author/Index/AuthorIndex.js @@ -94,13 +94,13 @@ class AuthorIndex extends Component { } = this.props; // Reset if not sorting by sortName - if (sortKey !== 'sortName') { + if (sortKey !== 'sortName' && sortKey !== 'sortNameLastFirst') { this.setState({ jumpBarItems: { order: [] } }); return; } const characters = _.reduce(items, (acc, item) => { - let char = item.sortName.charAt(0); + let char = item[sortKey].charAt(0); if (!isNaN(char)) { char = '#'; diff --git a/frontend/src/Author/Index/Menus/AuthorIndexSortMenu.js b/frontend/src/Author/Index/Menus/AuthorIndexSortMenu.js index cfad654aa..6432ec1a1 100644 --- a/frontend/src/Author/Index/Menus/AuthorIndexSortMenu.js +++ b/frontend/src/Author/Index/Menus/AuthorIndexSortMenu.js @@ -34,16 +34,16 @@ function AuthorIndexSortMenu(props) { sortDirection={sortDirection} onPress={onSortSelect} > - Name + First Name - Type + Last Name - {authorName} + {overviewOptions.showTitle === 'firstLast' ? authorName : authorNameLastFirst}
@@ -247,7 +248,8 @@ class AuthorIndexOverview extends Component { AuthorIndexOverview.propTypes = { id: PropTypes.number.isRequired, authorName: PropTypes.string.isRequired, - overview: PropTypes.string.isRequired, + authorNameLastFirst: PropTypes.string.isRequired, + overview: PropTypes.string, monitored: PropTypes.bool.isRequired, status: PropTypes.string.isRequired, titleSlug: PropTypes.string.isRequired, diff --git a/frontend/src/Author/Index/Overview/AuthorIndexOverviews.js b/frontend/src/Author/Index/Overview/AuthorIndexOverviews.js index 9cfac6096..1f0ced11f 100644 --- a/frontend/src/Author/Index/Overview/AuthorIndexOverviews.js +++ b/frontend/src/Author/Index/Overview/AuthorIndexOverviews.js @@ -90,7 +90,8 @@ class AuthorIndexOverviews extends Component { if (this._grid && (prevState.width !== width || prevState.rowHeight !== rowHeight || - hasDifferentItemsOrOrder(prevProps.items, items))) { + hasDifferentItemsOrOrder(prevProps.items, items) || + prevProps.overviewOptions.showTitle !== overviewOptions.showTitle)) { // recomputeGridSize also forces Grid to discard its cache of rendered cells this._grid.recomputeGridSize(); } @@ -101,7 +102,7 @@ class AuthorIndexOverviews extends Component { } if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) { - const index = getIndexOfFirstCharacter(items, jumpToCharacter); + const index = getIndexOfFirstCharacter(items, sortKey, jumpToCharacter); if (this._grid && index != null) { diff --git a/frontend/src/Author/Index/Overview/Options/AuthorIndexOverviewOptionsModalContent.js b/frontend/src/Author/Index/Overview/Options/AuthorIndexOverviewOptionsModalContent.js index f6b2d283e..783f28678 100644 --- a/frontend/src/Author/Index/Overview/Options/AuthorIndexOverviewOptionsModalContent.js +++ b/frontend/src/Author/Index/Overview/Options/AuthorIndexOverviewOptionsModalContent.js @@ -13,6 +13,11 @@ import ModalHeader from 'Components/Modal/ModalHeader'; import { inputTypes } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; +const nameOptions = [ + { key: 'firstLast', value: translate('NameFirstLast') }, + { key: 'lastFirst', value: translate('NameLastFirst') } +]; + const posterSizeOptions = [ { key: 'small', value: 'Small' }, { key: 'medium', value: 'Medium' }, @@ -28,6 +33,7 @@ class AuthorIndexOverviewOptionsModalContent extends Component { super(props, context); this.state = { + showTitle: props.showTitle, detailedProgressBar: props.detailedProgressBar, size: props.size, showMonitored: props.showMonitored, @@ -43,6 +49,7 @@ class AuthorIndexOverviewOptionsModalContent extends Component { componentDidUpdate(prevProps) { const { + showTitle, detailedProgressBar, size, showMonitored, @@ -57,6 +64,10 @@ class AuthorIndexOverviewOptionsModalContent extends Component { const state = {}; + if (showTitle !== prevProps.showTitle) { + state.showTitle = showTitle; + } + if (detailedProgressBar !== prevProps.detailedProgressBar) { state.detailedProgressBar = detailedProgressBar; } @@ -122,6 +133,7 @@ class AuthorIndexOverviewOptionsModalContent extends Component { } = this.props; const { + showTitle, detailedProgressBar, size, showMonitored, @@ -142,6 +154,20 @@ class AuthorIndexOverviewOptionsModalContent extends Component {
+ + + {translate('NameStyle')} + + + + + {translate('PosterSize')} @@ -291,6 +317,7 @@ class AuthorIndexOverviewOptionsModalContent extends Component { } AuthorIndexOverviewOptionsModalContent.propTypes = { + showTitle: PropTypes.string.isRequired, size: PropTypes.string.isRequired, detailedProgressBar: PropTypes.bool.isRequired, showMonitored: PropTypes.bool.isRequired, diff --git a/frontend/src/Author/Index/Posters/AuthorIndexPoster.js b/frontend/src/Author/Index/Posters/AuthorIndexPoster.js index 4c3b40300..3afeb4e8d 100644 --- a/frontend/src/Author/Index/Posters/AuthorIndexPoster.js +++ b/frontend/src/Author/Index/Posters/AuthorIndexPoster.js @@ -70,6 +70,7 @@ class AuthorIndexPoster extends Component { const { id, authorName, + authorNameLastFirst, monitored, titleSlug, status, @@ -193,9 +194,9 @@ class AuthorIndexPoster extends Component { /> { - showTitle && + showTitle !== 'no' &&
- {authorName} + {showTitle === 'firstLast' ? authorName : authorNameLastFirst}
} @@ -260,6 +261,7 @@ class AuthorIndexPoster extends Component { AuthorIndexPoster.propTypes = { id: PropTypes.number.isRequired, authorName: PropTypes.string.isRequired, + authorNameLastFirst: PropTypes.string.isRequired, monitored: PropTypes.bool.isRequired, status: PropTypes.string.isRequired, titleSlug: PropTypes.string.isRequired, @@ -269,7 +271,7 @@ AuthorIndexPoster.propTypes = { posterWidth: PropTypes.number.isRequired, posterHeight: PropTypes.number.isRequired, detailedProgressBar: PropTypes.bool.isRequired, - showTitle: PropTypes.bool.isRequired, + showTitle: PropTypes.string.isRequired, showMonitored: PropTypes.bool.isRequired, showQualityProfile: PropTypes.bool.isRequired, qualityProfile: PropTypes.object.isRequired, diff --git a/frontend/src/Author/Index/Posters/AuthorIndexPosters.js b/frontend/src/Author/Index/Posters/AuthorIndexPosters.js index 7675bdaae..e658a2ffe 100644 --- a/frontend/src/Author/Index/Posters/AuthorIndexPosters.js +++ b/frontend/src/Author/Index/Posters/AuthorIndexPosters.js @@ -50,7 +50,7 @@ function calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions) isSmallScreen ? columnPaddingSmallScreen : columnPadding ]; - if (showTitle) { + if (showTitle !== 'no') { heights.push(19); } @@ -137,7 +137,8 @@ class AuthorIndexPosters extends Component { prevState.columnWidth !== columnWidth || prevState.columnCount !== columnCount || prevState.rowHeight !== rowHeight || - hasDifferentItemsOrOrder(prevProps.items, items))) { + hasDifferentItemsOrOrder(prevProps.items, items)) || + prevProps.posterOptions.showTitle !== posterOptions.showTitle) { // recomputeGridSize also forces Grid to discard its cache of rendered cells this._grid.recomputeGridSize(); } @@ -148,7 +149,7 @@ class AuthorIndexPosters extends Component { } if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) { - const index = getIndexOfFirstCharacter(items, jumpToCharacter); + const index = getIndexOfFirstCharacter(items, sortKey, jumpToCharacter); if (this._grid && index != null) { const row = Math.floor(index / columnCount); diff --git a/frontend/src/Author/Index/Posters/Options/AuthorIndexPosterOptionsModalContent.js b/frontend/src/Author/Index/Posters/Options/AuthorIndexPosterOptionsModalContent.js index ec0add436..54ff06e16 100644 --- a/frontend/src/Author/Index/Posters/Options/AuthorIndexPosterOptionsModalContent.js +++ b/frontend/src/Author/Index/Posters/Options/AuthorIndexPosterOptionsModalContent.js @@ -19,6 +19,12 @@ const posterSizeOptions = [ { key: 'large', value: 'Large' } ]; +const nameOptions = [ + { key: 'no', value: translate('NoName') }, + { key: 'firstLast', value: translate('NameFirstLast') }, + { key: 'lastFirst', value: translate('NameLastFirst') } +]; + class AuthorIndexPosterOptionsModalContent extends Component { // @@ -148,9 +154,10 @@ class AuthorIndexPosterOptionsModalContent extends Component {
@@ -214,7 +221,7 @@ class AuthorIndexPosterOptionsModalContent extends Component { AuthorIndexPosterOptionsModalContent.propTypes = { size: PropTypes.string.isRequired, - showTitle: PropTypes.bool.isRequired, + showTitle: PropTypes.string.isRequired, showMonitored: PropTypes.bool.isRequired, showQualityProfile: PropTypes.bool.isRequired, detailedProgressBar: PropTypes.bool.isRequired, diff --git a/frontend/src/Author/Index/Table/AuthorIndexRow.js b/frontend/src/Author/Index/Table/AuthorIndexRow.js index 8517412e7..17fdff2b6 100644 --- a/frontend/src/Author/Index/Table/AuthorIndexRow.js +++ b/frontend/src/Author/Index/Table/AuthorIndexRow.js @@ -82,6 +82,7 @@ class AuthorIndexRow extends Component { monitored, status, authorName, + authorNameLastFirst, titleSlug, qualityProfile, metadataProfile, @@ -95,6 +96,7 @@ class AuthorIndexRow extends Component { tags, images, showBanners, + showTitle, showSearchAction, columns, isRefreshingAuthor, @@ -169,14 +171,14 @@ class AuthorIndexRow extends Component { { hasBannerError &&
- {authorName} + {showTitle === 'firstLast' ? authorName : authorNameLastFirst}
} : } @@ -408,6 +410,7 @@ AuthorIndexRow.propTypes = { monitored: PropTypes.bool.isRequired, status: PropTypes.string.isRequired, authorName: PropTypes.string.isRequired, + authorNameLastFirst: PropTypes.string.isRequired, titleSlug: PropTypes.string.isRequired, qualityProfile: PropTypes.object.isRequired, metadataProfile: PropTypes.object.isRequired, @@ -422,6 +425,7 @@ AuthorIndexRow.propTypes = { tags: PropTypes.arrayOf(PropTypes.number).isRequired, images: PropTypes.arrayOf(PropTypes.object).isRequired, showBanners: PropTypes.bool.isRequired, + showTitle: PropTypes.string.isRequired, showSearchAction: PropTypes.bool.isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired, isRefreshingAuthor: PropTypes.bool.isRequired, diff --git a/frontend/src/Author/Index/Table/AuthorIndexTable.js b/frontend/src/Author/Index/Table/AuthorIndexTable.js index 22e366dfd..e1147a4f8 100644 --- a/frontend/src/Author/Index/Table/AuthorIndexTable.js +++ b/frontend/src/Author/Index/Table/AuthorIndexTable.js @@ -25,12 +25,13 @@ class AuthorIndexTable extends Component { componentDidUpdate(prevProps) { const { items, + sortKey, jumpToCharacter } = this.props; if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) { - const scrollIndex = getIndexOfFirstCharacter(items, jumpToCharacter); + const scrollIndex = getIndexOfFirstCharacter(items, sortKey, jumpToCharacter); if (scrollIndex != null) { this.setState({ scrollIndex }); @@ -47,7 +48,8 @@ class AuthorIndexTable extends Component { const { items, columns, - showBanners + showBanners, + showTitle } = this.props; const author = items[rowIndex]; @@ -66,6 +68,7 @@ class AuthorIndexTable extends Component { qualityProfileId={author.qualityProfileId} metadataProfileId={author.metadataProfileId} showBanners={showBanners} + showTitle={showTitle} /> ); @@ -118,9 +121,10 @@ class AuthorIndexTable extends Component { AuthorIndexTable.propTypes = { items: PropTypes.arrayOf(PropTypes.object).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired, - sortKey: PropTypes.string, + sortKey: PropTypes.string.isRequired, sortDirection: PropTypes.oneOf(sortDirections.all), showBanners: PropTypes.bool.isRequired, + showTitle: PropTypes.string.isRequired, jumpToCharacter: PropTypes.string, scrollTop: PropTypes.number, scroller: PropTypes.instanceOf(Element).isRequired, diff --git a/frontend/src/Author/Index/Table/AuthorIndexTableConnector.js b/frontend/src/Author/Index/Table/AuthorIndexTableConnector.js index a774c8be6..52f120d36 100644 --- a/frontend/src/Author/Index/Table/AuthorIndexTableConnector.js +++ b/frontend/src/Author/Index/Table/AuthorIndexTableConnector.js @@ -12,6 +12,7 @@ function createMapStateToProps() { return { isSmallScreen: dimensions.isSmallScreen, showBanners: tableOptions.showBanners, + showTitle: tableOptions.showTitle, columns }; } diff --git a/frontend/src/Author/Index/Table/AuthorIndexTableOptions.js b/frontend/src/Author/Index/Table/AuthorIndexTableOptions.js index 9afcab118..d622621d0 100644 --- a/frontend/src/Author/Index/Table/AuthorIndexTableOptions.js +++ b/frontend/src/Author/Index/Table/AuthorIndexTableOptions.js @@ -6,6 +6,11 @@ import FormLabel from 'Components/Form/FormLabel'; import { inputTypes } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; +const nameOptions = [ + { key: 'firstLast', value: translate('NameFirstLast') }, + { key: 'lastFirst', value: translate('NameLastFirst') } +]; + class AuthorIndexTableOptions extends Component { // @@ -16,23 +21,27 @@ class AuthorIndexTableOptions extends Component { this.state = { showBanners: props.showBanners, - showSearchAction: props.showSearchAction + showSearchAction: props.showSearchAction, + showTitle: props.showTitle }; } componentDidUpdate(prevProps) { const { showBanners, - showSearchAction + showSearchAction, + showTitle } = this.props; if ( showBanners !== prevProps.showBanners || - showSearchAction !== prevProps.showSearchAction + showSearchAction !== prevProps.showSearchAction || + showTitle !== prevProps.showTitle ) { this.setState({ showBanners, - showSearchAction + showSearchAction, + showTitle }); } } @@ -59,11 +68,26 @@ class AuthorIndexTableOptions extends Component { render() { const { showBanners, - showSearchAction + showSearchAction, + showTitle } = this.state; return ( + + + {translate('NameStyle')} + + + + + {translate('ShowBanners')} @@ -97,6 +121,7 @@ class AuthorIndexTableOptions extends Component { } AuthorIndexTableOptions.propTypes = { + showTitle: PropTypes.string.isRequired, showBanners: PropTypes.bool.isRequired, showSearchAction: PropTypes.bool.isRequired, onTableOptionChange: PropTypes.func.isRequired diff --git a/frontend/src/Store/Actions/authorIndexActions.js b/frontend/src/Store/Actions/authorIndexActions.js index efa39cd97..e6b332fb9 100644 --- a/frontend/src/Store/Actions/authorIndexActions.js +++ b/frontend/src/Store/Actions/authorIndexActions.js @@ -16,31 +16,23 @@ export const section = 'authorIndex'; // State export const defaultState = { - sortKey: 'sortName', + sortKey: 'sortNameLastFirst', sortDirection: sortDirections.ASCENDING, - secondarySortKey: 'sortName', + secondarySortKey: 'sortNameLastFirst', secondarySortDirection: sortDirections.ASCENDING, view: 'posters', posterOptions: { detailedProgressBar: false, size: 'large', - showTitle: true, - showMonitored: true, - showQualityProfile: true, - showSearchAction: false - }, - - bannerOptions: { - detailedProgressBar: false, - size: 'large', - showTitle: false, + showTitle: 'lastFirst', showMonitored: true, showQualityProfile: true, showSearchAction: false }, overviewOptions: { + showTitle: 'lastFirst', detailedProgressBar: false, size: 'medium', showMonitored: true, @@ -54,6 +46,7 @@ export const defaultState = { }, tableOptions: { + showTitle: 'lastFirst', showBanners: false, showSearchAction: false }, diff --git a/frontend/src/Store/Selectors/createAuthorClientSideCollectionItemsSelector.js b/frontend/src/Store/Selectors/createAuthorClientSideCollectionItemsSelector.js index 2223ce818..ee67432ac 100644 --- a/frontend/src/Store/Selectors/createAuthorClientSideCollectionItemsSelector.js +++ b/frontend/src/Store/Selectors/createAuthorClientSideCollectionItemsSelector.js @@ -9,12 +9,14 @@ function createUnoptimizedSelector(uiSection) { const items = authors.items.map((s) => { const { id, - sortName + sortName, + sortNameLastFirst } = s; return { id, - sortName + sortName, + sortNameLastFirst }; }); diff --git a/frontend/src/Utilities/Array/getIndexOfFirstCharacter.js b/frontend/src/Utilities/Array/getIndexOfFirstCharacter.js index d27cfd604..57dea6c76 100644 --- a/frontend/src/Utilities/Array/getIndexOfFirstCharacter.js +++ b/frontend/src/Utilities/Array/getIndexOfFirstCharacter.js @@ -1,8 +1,8 @@ import _ from 'lodash'; -export default function getIndexOfFirstCharacter(items, character) { +export default function getIndexOfFirstCharacter(items, sortKey, character) { return _.findIndex(items, (item) => { - const firstCharacter = item.sortName.charAt(0); + const firstCharacter = item[sortKey].charAt(0); if (character === '#') { return !isNaN(firstCharacter); diff --git a/src/NzbDrone.Common.Test/ExtensionTests/StringExtensionTests/ToSortNameFixture.cs b/src/NzbDrone.Common.Test/ExtensionTests/StringExtensionTests/ToSortNameFixture.cs index 805df31b4..1009867e8 100644 --- a/src/NzbDrone.Common.Test/ExtensionTests/StringExtensionTests/ToSortNameFixture.cs +++ b/src/NzbDrone.Common.Test/ExtensionTests/StringExtensionTests/ToSortNameFixture.cs @@ -36,7 +36,7 @@ public void should_remove_brackets(string input, string expected) [TestCase("John [x]von Neumann (III)", "von Neumann, John")] public void should_get_sort_name(string input, string expected) { - input.ToSortName().Should().Be(expected); + input.ToLastFirst().Should().Be(expected); } } } diff --git a/src/NzbDrone.Common/Extensions/StringExtensions.cs b/src/NzbDrone.Common/Extensions/StringExtensions.cs index 77f53b4e1..923883056 100644 --- a/src/NzbDrone.Common/Extensions/StringExtensions.cs +++ b/src/NzbDrone.Common/Extensions/StringExtensions.cs @@ -275,7 +275,7 @@ public static string RemoveBracketedText(this string input) return new string(buf.ToArray()); } - public static string ToSortName(this string author) + public static string ToLastFirst(this string author) { // ported from https://github.com/kovidgoyal/calibre/blob/master/src/calibre/ebooks/metadata/__init__.py if (author == null) diff --git a/src/NzbDrone.Core/Books/Model/AuthorMetadata.cs b/src/NzbDrone.Core/Books/Model/AuthorMetadata.cs index bda86e873..09eadd8a8 100644 --- a/src/NzbDrone.Core/Books/Model/AuthorMetadata.cs +++ b/src/NzbDrone.Core/Books/Model/AuthorMetadata.cs @@ -20,6 +20,8 @@ public AuthorMetadata() public string TitleSlug { get; set; } public string Name { get; set; } public string SortName { get; set; } + public string NameLastFirst { get; set; } + public string SortNameLastFirst { get; set; } public List Aliases { get; set; } public string Overview { get; set; } public string Disambiguation { get; set; } @@ -43,7 +45,9 @@ public override void UseMetadataFrom(AuthorMetadata other) ForeignAuthorId = other.ForeignAuthorId; TitleSlug = other.TitleSlug; Name = other.Name; + NameLastFirst = other.NameLastFirst; SortName = other.SortName; + SortNameLastFirst = other.SortNameLastFirst; Aliases = other.Aliases; Overview = other.Overview.IsNullOrWhiteSpace() ? Overview : other.Overview; Disambiguation = other.Disambiguation; diff --git a/src/NzbDrone.Core/Books/Services/AuthorService.cs b/src/NzbDrone.Core/Books/Services/AuthorService.cs index e9a79c56c..9c9338076 100644 --- a/src/NzbDrone.Core/Books/Services/AuthorService.cs +++ b/src/NzbDrone.Core/Books/Services/AuthorService.cs @@ -101,7 +101,7 @@ public List, string>> AuthorScoringFunctions( { tc((a, t) => a.CleanName.FuzzyMatch(t), cleanTitle), tc((a, t) => a.Name.FuzzyMatch(t), title), - tc((a, t) => a.Name.ToSortName().FuzzyMatch(t), title), + tc((a, t) => a.Name.ToLastFirst().FuzzyMatch(t), title), tc((a, t) => a.Metadata.Value.Aliases.Concat(new List { a.Name }).Max(x => x.CleanAuthorName().FuzzyMatch(t)), cleanTitle), }; @@ -153,7 +153,7 @@ public List, string>> ReportAuthorScoringFunc { tc((a, t) => t.FuzzyContains(a.CleanName), cleanReportTitle), tc((a, t) => t.FuzzyContains(a.Metadata.Value.Name), reportTitle), - tc((a, t) => t.FuzzyContains(a.Metadata.Value.Name.ToSortName()), reportTitle) + tc((a, t) => t.FuzzyContains(a.Metadata.Value.Name.ToLastFirst()), reportTitle) }; return scoringFunctions; diff --git a/src/NzbDrone.Core/Datastore/Migration/009_update_author_sort_name.cs b/src/NzbDrone.Core/Datastore/Migration/009_update_author_sort_name.cs index d1a101fde..95948bfc5 100644 --- a/src/NzbDrone.Core/Datastore/Migration/009_update_author_sort_name.cs +++ b/src/NzbDrone.Core/Datastore/Migration/009_update_author_sort_name.cs @@ -25,7 +25,7 @@ private void MigrateAuthorSortName(IDbConnection conn, IDbTransaction tran) foreach (var row in rows) { - row.SortName = row.Name.ToSortName().ToLower(); + row.SortName = row.Name.ToLastFirst().ToLower(); } var sql = "UPDATE AuthorMetadata SET SortName = @SortName WHERE Id = @Id"; diff --git a/src/NzbDrone.Core/Datastore/Migration/013_update_author_sort_name_again.cs b/src/NzbDrone.Core/Datastore/Migration/013_update_author_sort_name_again.cs new file mode 100644 index 000000000..eee19eaee --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/013_update_author_sort_name_again.cs @@ -0,0 +1,45 @@ +using System.Data; +using System.Linq; +using Dapper; +using FluentMigrator; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(013)] + public class update_author_sort_name_again : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("AuthorMetadata").AddColumn("NameLastFirst").AsString().Nullable(); + Alter.Table("AuthorMetadata").AddColumn("SortNameLastFirst").AsString().Nullable(); + Execute.WithConnection(MigrateAuthorSortName); + Alter.Table("AuthorMetadata").AlterColumn("NameLastFirst").AsString().NotNullable(); + Alter.Table("AuthorMetadata").AlterColumn("SortNameLastFirst").AsString().NotNullable(); + } + + private void MigrateAuthorSortName(IDbConnection conn, IDbTransaction tran) + { + var rows = conn.Query("SELECT AuthorMetadata.Id, AuthorMetadata.Name FROM AuthorMetadata", transaction: tran); + + foreach (var row in rows) + { + row.NameLastFirst = row.Name.ToLastFirst(); + row.SortName = row.Name.ToLower(); + row.SortNameLastFirst = row.Name.ToLastFirst().ToLower(); + } + + var sql = "UPDATE AuthorMetadata SET NameLastFirst = @NameLastFirst, SortName = @SortName, SortNameLastFirst = @SortNameLastFirst WHERE Id = @Id"; + conn.Execute(sql, rows, transaction: tran); + } + + private class AuthorName : ModelBase + { + public string Name { get; set; } + public string NameLastFirst { get; set; } + public string SortName { get; set; } + public string SortNameLastFirst { get; set; } + } + } +} diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 45e7bebcf..8f91bc8ed 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -381,6 +381,9 @@ "MustContain": "Must Contain", "MustNotContain": "Must Not Contain", "Name": "Name", + "NameFirstLast": "First Name Last Name", + "NameLastFirst": "Last Name, First Name", + "NameStyle": "Author Name Style", "NamingSettings": "Naming Settings", "NETCore": ".NET Core", "New": "New", @@ -390,6 +393,7 @@ "NoLimitForAnyRuntime": "No limit for any runtime", "NoLogFiles": "No log files", "NoMinimumForAnyRuntime": "No minimum for any runtime", + "NoName": "Do not show name", "None": "None", "NoTagsHaveBeenAddedYet": "No tags have been added yet. Add tags to link authors with delay profiles, restrictions, or notifications. Click {0} to find out more about tags in Readarr.", "NotificationTriggers": "Notification Triggers", diff --git a/src/NzbDrone.Core/MetadataSource/Goodreads/GoodreadsProxy.cs b/src/NzbDrone.Core/MetadataSource/Goodreads/GoodreadsProxy.cs index a9bf9becc..eb74fe928 100644 --- a/src/NzbDrone.Core/MetadataSource/Goodreads/GoodreadsProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/Goodreads/GoodreadsProxy.cs @@ -531,7 +531,9 @@ private static AuthorMetadata MapAuthor(AuthorResource resource) Status = resource.DiedOnDate < DateTime.UtcNow ? AuthorStatusType.Ended : AuthorStatusType.Continuing }; - author.SortName = author.Name.ToSortName().ToLower(); + author.SortName = author.Name.ToLower(); + author.NameLastFirst = author.Name.ToLastFirst(); + author.SortNameLastFirst = author.NameLastFirst.ToLower(); if (!NoPhotoRegex.IsMatch(resource.LargeImageUrl)) { @@ -556,7 +558,9 @@ private static AuthorMetadata MapAuthor(AuthorSummaryResource resource) TitleSlug = resource.Id.ToString() }; - author.SortName = author.Name.ToSortName().ToLower(); + author.SortName = author.Name.ToLower(); + author.NameLastFirst = author.Name.ToLastFirst(); + author.SortNameLastFirst = author.NameLastFirst.ToLower(); if (resource.RatingsCount.HasValue) { @@ -707,7 +711,9 @@ private Book MapSearchResult(WorkResource resource) { ForeignAuthorId = resource.BestBook.AuthorId.ToString(), Name = resource.BestBook.AuthorName, - SortName = resource.BestBook.AuthorName.ToSortName().ToLower(), + NameLastFirst = resource.BestBook.AuthorName.ToLastFirst(), + SortName = resource.BestBook.AuthorName.ToLower(), + SortNameLastFirst = resource.BestBook.AuthorName.ToLastFirst().ToLower(), TitleSlug = resource.BestBook.AuthorId.ToString() } }; diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs index f44d2be5f..bb7189a81 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs @@ -315,7 +315,9 @@ private static AuthorMetadata MapAuthor(AuthorSummaryResource resource) Ratings = new Ratings { Votes = resource.RatingsCount, Value = (decimal)resource.AverageRating } }; - author.SortName = author.Name.ToSortName().ToLower(); + author.NameLastFirst = author.Name.ToLastFirst(); + author.SortName = author.Name.ToLower(); + author.SortNameLastFirst = author.Name.ToLastFirst().ToLower(); if (resource.ImageUrl.IsNotNullOrWhiteSpace()) { diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index c83273247..0d3043fab 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -221,7 +221,7 @@ private void AddAuthorTokens(Dictionary> tokenH tokenHandlers["{Author Name}"] = m => author.Name; tokenHandlers["{Author CleanName}"] = m => CleanTitle(author.Name); tokenHandlers["{Author NameThe}"] = m => TitleThe(author.Name); - tokenHandlers["{Author SortName}"] = m => author.Name.ToSortName(); + tokenHandlers["{Author SortName}"] = m => author.Metadata.Value.NameLastFirst; if (author.Metadata.Value.Disambiguation != null) { diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 3e6cf8891..36d847453 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -357,7 +357,7 @@ public static ParsedBookInfo ParseBookTitleWithSearchCriteria(string title, Auth if (foundAuthor == null) { - foundAuthor = GetTitleFuzzy(simpleTitle, authorName.ToSortName(), out remainder); + foundAuthor = GetTitleFuzzy(simpleTitle, authorName.ToLastFirst(), out remainder); } var foundBook = GetTitleFuzzy(remainder, bestBook.Title, out _); diff --git a/src/Readarr.Api.V1/Author/AuthorResource.cs b/src/Readarr.Api.V1/Author/AuthorResource.cs index 49c4eaa50..be4bf0035 100644 --- a/src/Readarr.Api.V1/Author/AuthorResource.cs +++ b/src/Readarr.Api.V1/Author/AuthorResource.cs @@ -22,6 +22,7 @@ public class AuthorResource : RestResource public bool Ended => Status == AuthorStatusType.Ended; public string AuthorName { get; set; } + public string AuthorNameLastFirst { get; set; } public string ForeignAuthorId { get; set; } public string TitleSlug { get; set; } public string Overview { get; set; } @@ -47,6 +48,8 @@ public class AuthorResource : RestResource public List Genres { get; set; } public string CleanName { get; set; } public string SortName { get; set; } + public string SortNameLastFirst { get; set; } + public HashSet Tags { get; set; } public DateTime Added { get; set; } public AddAuthorOptions AddOptions { get; set; } @@ -70,9 +73,11 @@ public static AuthorResource ToResource(this NzbDrone.Core.Books.Author model) AuthorMetadataId = model.AuthorMetadataId, AuthorName = model.Name, + AuthorNameLastFirst = model.Metadata.Value.NameLastFirst, //AlternateTitles SortName = model.Metadata.Value.SortName, + SortNameLastFirst = model.Metadata.Value.SortNameLastFirst, Status = model.Metadata.Value.Status, Overview = model.Metadata.Value.Overview, @@ -119,7 +124,9 @@ public static NzbDrone.Core.Books.Author ToModel(this AuthorResource resource) ForeignAuthorId = resource.ForeignAuthorId, TitleSlug = resource.TitleSlug, Name = resource.AuthorName, + NameLastFirst = resource.AuthorNameLastFirst, SortName = resource.SortName, + SortNameLastFirst = resource.SortNameLastFirst, Status = resource.Status, Overview = resource.Overview, Links = resource.Links, diff --git a/src/Readarr.Api.V1/Queue/QueueController.cs b/src/Readarr.Api.V1/Queue/QueueController.cs index a4db4b6ec..e8d8b5564 100644 --- a/src/Readarr.Api.V1/Queue/QueueController.cs +++ b/src/Readarr.Api.V1/Queue/QueueController.cs @@ -173,6 +173,8 @@ public PagingResource GetQueue(bool includeUnknownAuthorItems = f return q => q.Status; case "authors.sortName": return q => q.Author?.Metadata.Value.SortName ?? string.Empty; + case "authors.sortNameLastFirst": + return q => q.Author?.Metadata.Value.SortNameLastFirst ?? string.Empty; case "title": return q => q.Title; case "book":