From f819e582cfd42d88fdfccebc89a1c145ba0ba7a3 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 3 Aug 2019 18:55:31 -0700 Subject: [PATCH] New: Author folder hint when selecting a root folder while adding a new author (cherry picked from commit dd09f31abb4dd3f699bcff0a47577075300c70ee) Fix AuthorFolderAsRootFolderValidator (cherry picked from commit 0ce81e1ab69d43fde382cc4ae22cd46fe626dea7) --- frontend/src/Author/NoAuthor.js | 5 +- .../Form/RootFolderSelectInputConnector.js | 7 +-- .../Form/RootFolderSelectInputOption.css | 9 ++++ .../Form/RootFolderSelectInputOption.css.d.ts | 2 + .../Form/RootFolderSelectInputOption.js | 30 ++++++++++-- .../RootFolderSelectInputSelectedValue.css | 12 ++++- ...ootFolderSelectInputSelectedValue.css.d.ts | 2 + .../RootFolderSelectInputSelectedValue.js | 24 +++++++-- frontend/src/Search/AddNewItem.css | 3 +- frontend/src/Search/AddNewItem.js | 25 +++++++++- frontend/src/Search/AddNewItemConnector.js | 6 ++- .../Search/Author/AddNewAuthorModalContent.js | 5 +- .../AddNewAuthorModalContentConnector.js | 5 +- .../Search/Author/AddNewAuthorSearchResult.js | 3 ++ .../src/Search/Book/AddNewBookModalContent.js | 3 +- .../Book/AddNewBookModalContentConnector.js | 5 +- .../src/Search/Book/AddNewBookSearchResult.js | 1 + .../src/Search/Common/AddAuthorOptionsForm.js | 17 +++++++ .../Extensions/PathExtensions.cs | 9 ++++ src/NzbDrone.Core/Localization/Core/en.json | 4 ++ src/Readarr.Api.V1/Author/AuthorController.cs | 8 ++- .../AuthorFolderAsRootFolderValidator.cs | 49 +++++++++++++++++++ .../Author/AuthorLookupController.cs | 7 ++- src/Readarr.Api.V1/Author/AuthorResource.cs | 1 + .../RootFolders/RootFolderResource.cs | 3 +- src/Readarr.Api.V1/Search/SearchController.cs | 9 +++- 26 files changed, 225 insertions(+), 29 deletions(-) create mode 100644 src/Readarr.Api.V1/Author/AuthorFolderAsRootFolderValidator.cs diff --git a/frontend/src/Author/NoAuthor.js b/frontend/src/Author/NoAuthor.js index a411b9545..009b21c82 100644 --- a/frontend/src/Author/NoAuthor.js +++ b/frontend/src/Author/NoAuthor.js @@ -2,6 +2,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import Button from 'Components/Link/Button'; import { kinds } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; import styles from './NoAuthor.css'; function NoAuthor(props) { @@ -31,7 +32,7 @@ function NoAuthor(props) { to="/settings/mediamanagement" kind={kinds.PRIMARY} > - Add Root Folder + {translate('AddRootFolder')} @@ -40,7 +41,7 @@ function NoAuthor(props) { to="/add/search" kind={kinds.PRIMARY} > - Add New Author + {translate('AddNewAuthor')} diff --git a/frontend/src/Components/Form/RootFolderSelectInputConnector.js b/frontend/src/Components/Form/RootFolderSelectInputConnector.js index 083b11448..50c8b057f 100644 --- a/frontend/src/Components/Form/RootFolderSelectInputConnector.js +++ b/frontend/src/Components/Form/RootFolderSelectInputConnector.js @@ -28,8 +28,7 @@ function createMapStateToProps() { if (includeNoChange) { values.unshift({ key: 'noChange', - value: '', - name: translate('NoChange'), + value: translate('NoChange'), isDisabled: includeNoChangeDisabled, isMissing: false }); @@ -39,7 +38,6 @@ function createMapStateToProps() { values.push({ key: '', value: '', - name: '', isDisabled: true, isHidden: true }); @@ -56,8 +54,7 @@ function createMapStateToProps() { values.push({ key: ADD_NEW_KEY, - value: '', - name: 'Add a new path' + value: 'Add a new path' }); return { diff --git a/frontend/src/Components/Form/RootFolderSelectInputOption.css b/frontend/src/Components/Form/RootFolderSelectInputOption.css index 0c62c6646..09dd70b02 100644 --- a/frontend/src/Components/Form/RootFolderSelectInputOption.css +++ b/frontend/src/Components/Form/RootFolderSelectInputOption.css @@ -13,6 +13,15 @@ } } +.value { + display: flex; +} + +.authorFolder { + flex: 0 0 auto; + color: var(--disabledColor); +} + .freeSpace { margin-left: 15px; color: var(--darkGray); diff --git a/frontend/src/Components/Form/RootFolderSelectInputOption.css.d.ts b/frontend/src/Components/Form/RootFolderSelectInputOption.css.d.ts index 327affebe..ecbd40d7d 100644 --- a/frontend/src/Components/Form/RootFolderSelectInputOption.css.d.ts +++ b/frontend/src/Components/Form/RootFolderSelectInputOption.css.d.ts @@ -1,10 +1,12 @@ // This file is automatically generated. // Please do not change this file! interface CssExports { + 'authorFolder': string; 'freeSpace': string; 'isMissing': string; 'isMobile': string; 'optionText': string; + 'value': string; } export const cssExports: CssExports; export default cssExports; diff --git a/frontend/src/Components/Form/RootFolderSelectInputOption.js b/frontend/src/Components/Form/RootFolderSelectInputOption.js index 00825f993..1921fb091 100644 --- a/frontend/src/Components/Form/RootFolderSelectInputOption.js +++ b/frontend/src/Components/Form/RootFolderSelectInputOption.js @@ -7,18 +7,24 @@ import styles from './RootFolderSelectInputOption.css'; function RootFolderSelectInputOption(props) { const { + id, value, name, freeSpace, + authorFolder, isMissing, isMobile, + isWindows, ...otherProps } = props; - const text = value === '' ? name : `${name} [${value}]`; + const slashCharacter = isWindows ? '\\' : '/'; + + const text = name === '' ? value : `[${name}] ${value}`; return ( @@ -27,7 +33,18 @@ function RootFolderSelectInputOption(props) { isMobile && styles.isMobile )} > -
{text}
+
+ {text} + + { + authorFolder && id !== 'addNew' ? +
+ {slashCharacter} + {authorFolder} +
: + null + } +
{ freeSpace == null ? @@ -50,11 +67,18 @@ function RootFolderSelectInputOption(props) { } RootFolderSelectInputOption.propTypes = { + id: PropTypes.string.isRequired, name: PropTypes.string.isRequired, value: PropTypes.string.isRequired, freeSpace: PropTypes.number, + authorFolder: PropTypes.string, isMissing: PropTypes.bool, - isMobile: PropTypes.bool.isRequired + isMobile: PropTypes.bool.isRequired, + isWindows: PropTypes.bool +}; + +RootFolderSelectInputOption.defaultProps = { + name: '' }; export default RootFolderSelectInputOption; diff --git a/frontend/src/Components/Form/RootFolderSelectInputSelectedValue.css b/frontend/src/Components/Form/RootFolderSelectInputSelectedValue.css index 86107a624..281b6b0f6 100644 --- a/frontend/src/Components/Form/RootFolderSelectInputSelectedValue.css +++ b/frontend/src/Components/Form/RootFolderSelectInputSelectedValue.css @@ -7,10 +7,20 @@ overflow: hidden; } +.pathContainer { + @add-mixin truncate; + display: flex; + flex: 1 0 0; +} + .path { @add-mixin truncate; + flex: 0 1 auto; +} - flex: 1 0 0; +.authorFolder { + flex: 0 1 auto; + color: var(--disabledColor); } .freeSpace { diff --git a/frontend/src/Components/Form/RootFolderSelectInputSelectedValue.css.d.ts b/frontend/src/Components/Form/RootFolderSelectInputSelectedValue.css.d.ts index 682ac91de..f06abab1c 100644 --- a/frontend/src/Components/Form/RootFolderSelectInputSelectedValue.css.d.ts +++ b/frontend/src/Components/Form/RootFolderSelectInputSelectedValue.css.d.ts @@ -1,8 +1,10 @@ // This file is automatically generated. // Please do not change this file! interface CssExports { + 'authorFolder': string; 'freeSpace': string; 'path': string; + 'pathContainer': string; 'selectedValue': string; } export const cssExports: CssExports; diff --git a/frontend/src/Components/Form/RootFolderSelectInputSelectedValue.js b/frontend/src/Components/Form/RootFolderSelectInputSelectedValue.js index 0af2f61ae..7081effc2 100644 --- a/frontend/src/Components/Form/RootFolderSelectInputSelectedValue.js +++ b/frontend/src/Components/Form/RootFolderSelectInputSelectedValue.js @@ -9,19 +9,34 @@ function RootFolderSelectInputSelectedValue(props) { name, value, freeSpace, + authorFolder, includeFreeSpace, + isWindows, ...otherProps } = props; - const text = value === '' ? name : `${name} [${value}]`; + const slashCharacter = isWindows ? '\\' : '/'; + + const text = name === '' ? value : `[${name}] ${value}`; return ( -
- {text} +
+
+ {text} +
+ + { + authorFolder ? +
+ {slashCharacter} + {authorFolder} +
: + null + }
{ @@ -38,10 +53,13 @@ RootFolderSelectInputSelectedValue.propTypes = { name: PropTypes.string, value: PropTypes.string, freeSpace: PropTypes.number, + authorFolder: PropTypes.string, + isWindows: PropTypes.bool, includeFreeSpace: PropTypes.bool.isRequired }; RootFolderSelectInputSelectedValue.defaultProps = { + name: '', includeFreeSpace: true }; diff --git a/frontend/src/Search/AddNewItem.css b/frontend/src/Search/AddNewItem.css index d587bfbb8..489a67830 100644 --- a/frontend/src/Search/AddNewItem.css +++ b/frontend/src/Search/AddNewItem.css @@ -35,11 +35,12 @@ .message { margin-top: 30px; text-align: center; + font-weight: 300; + font-size: $largeFontSize; } .helpText { margin-bottom: 10px; - font-weight: 300; font-size: 24px; } diff --git a/frontend/src/Search/AddNewItem.js b/frontend/src/Search/AddNewItem.js index 1f1a87fc7..82cf6c6c1 100644 --- a/frontend/src/Search/AddNewItem.js +++ b/frontend/src/Search/AddNewItem.js @@ -82,7 +82,8 @@ class AddNewItem extends Component { render() { const { error, - items + items, + hasExistingAuthors } = this.props; const term = this.state.term; @@ -186,7 +187,8 @@ class AddNewItem extends Component { } { - !term && + term ? + null :
{translate('ItsEasyToAddANewAuthorOrBookJustStartTypingTheNameOfTheItemYouWantToAdd')} @@ -199,6 +201,24 @@ class AddNewItem extends Component {
} + { + !term && !hasExistingAuthors ? +
+
+ You haven't added any authors yet, do you want to add an existing library location (Root Folder) and update? +
+
+ +
+
: + null + } +
@@ -213,6 +233,7 @@ AddNewItem.propTypes = { isAdding: PropTypes.bool.isRequired, addError: PropTypes.object, items: PropTypes.arrayOf(PropTypes.object).isRequired, + hasExistingAuthors: PropTypes.bool.isRequired, onSearchChange: PropTypes.func.isRequired, onClearSearch: PropTypes.func.isRequired }; diff --git a/frontend/src/Search/AddNewItemConnector.js b/frontend/src/Search/AddNewItemConnector.js index d1064777d..806e1559a 100644 --- a/frontend/src/Search/AddNewItemConnector.js +++ b/frontend/src/Search/AddNewItemConnector.js @@ -10,13 +10,15 @@ import AddNewItem from './AddNewItem'; function createMapStateToProps() { return createSelector( (state) => state.search, + (state) => state.authors.items.length, (state) => state.router.location, - (search, location) => { + (search, existingAuthorsCount, location) => { const { params } = parseUrl(location.search); return { + ...search, term: params.term, - ...search + hasExistingAuthors: existingAuthorsCount > 0 }; } ); diff --git a/frontend/src/Search/Author/AddNewAuthorModalContent.js b/frontend/src/Search/Author/AddNewAuthorModalContent.js index 70d8f9c19..f91ef458f 100644 --- a/frontend/src/Search/Author/AddNewAuthorModalContent.js +++ b/frontend/src/Search/Author/AddNewAuthorModalContent.js @@ -9,6 +9,7 @@ import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; import { kinds } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; import AddAuthorOptionsForm from '../Common/AddAuthorOptionsForm.js'; import styles from './AddNewAuthorModalContent.css'; @@ -54,7 +55,7 @@ class AddNewAuthorModalContent extends Component { return ( - Add new Author + {translate('AddNewAuthor')} @@ -133,7 +134,7 @@ class AddNewAuthorModalContent extends Component { AddNewAuthorModalContent.propTypes = { authorName: PropTypes.string.isRequired, - disambiguation: PropTypes.string.isRequired, + disambiguation: PropTypes.string, overview: PropTypes.string, images: PropTypes.arrayOf(PropTypes.object).isRequired, isAdding: PropTypes.bool.isRequired, diff --git a/frontend/src/Search/Author/AddNewAuthorModalContentConnector.js b/frontend/src/Search/Author/AddNewAuthorModalContentConnector.js index 0b9c298ef..f240d21cc 100644 --- a/frontend/src/Search/Author/AddNewAuthorModalContentConnector.js +++ b/frontend/src/Search/Author/AddNewAuthorModalContentConnector.js @@ -4,6 +4,7 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { addAuthor, setAuthorAddDefault } from 'Store/Actions/searchActions'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; +import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector'; import selectSettings from 'Store/Selectors/selectSettings'; import AddNewAuthorModalContent from './AddNewAuthorModalContent'; @@ -12,7 +13,8 @@ function createMapStateToProps() { (state) => state.search, (state) => state.settings.metadataProfiles, createDimensionsSelector(), - (searchState, metadataProfiles, dimensions) => { + createSystemStatusSelector(), + (searchState, metadataProfiles, dimensions, systemStatus) => { const { isAdding, addError, @@ -32,6 +34,7 @@ function createMapStateToProps() { isSmallScreen: dimensions.isSmallScreen, validationErrors, validationWarnings, + isWindows: systemStatus.isWindows, ...settings }; } diff --git a/frontend/src/Search/Author/AddNewAuthorSearchResult.js b/frontend/src/Search/Author/AddNewAuthorSearchResult.js index 8882ee5c0..df2f55ca7 100644 --- a/frontend/src/Search/Author/AddNewAuthorSearchResult.js +++ b/frontend/src/Search/Author/AddNewAuthorSearchResult.js @@ -78,6 +78,7 @@ class AddNewAuthorSearchResult extends Component { status, overview, ratings, + folder, images, isExistingAuthor, isSmallScreen @@ -205,6 +206,7 @@ class AddNewAuthorSearchResult extends Component { disambiguation={disambiguation} year={year} overview={overview} + folder={folder} images={images} onModalClose={this.onAddAuthorModalClose} /> @@ -222,6 +224,7 @@ AddNewAuthorSearchResult.propTypes = { status: PropTypes.string.isRequired, overview: PropTypes.string, ratings: PropTypes.object.isRequired, + folder: PropTypes.string.isRequired, images: PropTypes.arrayOf(PropTypes.object).isRequired, isExistingAuthor: PropTypes.bool.isRequired, isSmallScreen: PropTypes.bool.isRequired diff --git a/frontend/src/Search/Book/AddNewBookModalContent.js b/frontend/src/Search/Book/AddNewBookModalContent.js index 003dc085d..d06761979 100644 --- a/frontend/src/Search/Book/AddNewBookModalContent.js +++ b/frontend/src/Search/Book/AddNewBookModalContent.js @@ -10,6 +10,7 @@ import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; import { kinds } from 'Helpers/Props'; import stripHtml from 'Utilities/String/stripHtml'; +import translate from 'Utilities/String/translate'; import AddAuthorOptionsForm from '../Common/AddAuthorOptionsForm.js'; import styles from './AddNewBookModalContent.css'; @@ -58,7 +59,7 @@ class AddNewBookModalContent extends Component { return ( - Add new Book + {translate('AddNewBook')} diff --git a/frontend/src/Search/Book/AddNewBookModalContentConnector.js b/frontend/src/Search/Book/AddNewBookModalContentConnector.js index 762374946..37030528c 100644 --- a/frontend/src/Search/Book/AddNewBookModalContentConnector.js +++ b/frontend/src/Search/Book/AddNewBookModalContentConnector.js @@ -4,6 +4,7 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { addBook, setBookAddDefault } from 'Store/Actions/searchActions'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; +import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector'; import selectSettings from 'Store/Selectors/selectSettings'; import AddNewBookModalContent from './AddNewBookModalContent'; @@ -13,7 +14,8 @@ function createMapStateToProps() { (state) => state.search, (state) => state.settings.metadataProfiles, createDimensionsSelector(), - (isExistingAuthor, searchState, metadataProfiles, dimensions) => { + createSystemStatusSelector(), + (isExistingAuthor, searchState, metadataProfiles, dimensions, systemStatus) => { const { isAdding, addError, @@ -33,6 +35,7 @@ function createMapStateToProps() { isSmallScreen: dimensions.isSmallScreen, validationErrors, validationWarnings, + isWindows: systemStatus.isWindows, ...settings }; } diff --git a/frontend/src/Search/Book/AddNewBookSearchResult.js b/frontend/src/Search/Book/AddNewBookSearchResult.js index a71d15c91..bdae9c382 100644 --- a/frontend/src/Search/Book/AddNewBookSearchResult.js +++ b/frontend/src/Search/Book/AddNewBookSearchResult.js @@ -203,6 +203,7 @@ class AddNewBookSearchResult extends Component { disambiguation={disambiguation} authorName={author.authorName} overview={overview} + folder={author.folder} images={images} onModalClose={this.onAddBookModalClose} /> diff --git a/frontend/src/Search/Common/AddAuthorOptionsForm.js b/frontend/src/Search/Common/AddAuthorOptionsForm.js index 68a310172..92503986e 100644 --- a/frontend/src/Search/Common/AddAuthorOptionsForm.js +++ b/frontend/src/Search/Common/AddAuthorOptionsForm.js @@ -39,7 +39,9 @@ class AddAuthorOptionsForm extends Component { includeNoneMetadataProfile, includeSpecificBookMonitor, showMetadataProfile, + folder, tags, + isWindows, onInputChange, ...otherProps } = this.props; @@ -54,6 +56,15 @@ class AddAuthorOptionsForm extends Component { @@ -179,8 +190,14 @@ AddAuthorOptionsForm.propTypes = { showMetadataProfile: PropTypes.bool.isRequired, includeNoneMetadataProfile: PropTypes.bool.isRequired, includeSpecificBookMonitor: PropTypes.bool.isRequired, + folder: PropTypes.string.isRequired, tags: PropTypes.object.isRequired, + isWindows: PropTypes.bool.isRequired, onInputChange: PropTypes.func.isRequired }; +AddAuthorOptionsForm.defaultProps = { + includeSpecificBookMonitor: false +}; + export default AddAuthorOptionsForm; diff --git a/src/NzbDrone.Common/Extensions/PathExtensions.cs b/src/NzbDrone.Common/Extensions/PathExtensions.cs index 17cd6a49a..6d850636a 100644 --- a/src/NzbDrone.Common/Extensions/PathExtensions.cs +++ b/src/NzbDrone.Common/Extensions/PathExtensions.cs @@ -108,6 +108,15 @@ public static string GetParentPath(this string childPath) return Directory.GetParent(cleanPath)?.FullName; } + public static string GetCleanPath(this string path) + { + var cleanPath = OsInfo.IsWindows + ? PARENT_PATH_END_SLASH_REGEX.Replace(path, "") + : path.TrimEnd(Path.DirectorySeparatorChar); + + return cleanPath; + } + public static bool IsParentPath(this string parentPath, string childPath) { if (parentPath != "/" && !parentPath.EndsWith(":\\")) diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index ea034cbb1..55e8ed679 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -11,7 +11,11 @@ "AddListExclusion": "Add List Exclusion", "AddMissing": "Add missing", "AddNew": "Add New", + "AddNewBook": "Add New Book", + "AddNewAuthor": "Add New Author", + "AddNewAuthorRootFolderHelpText": "'{folder}' subfolder will be created automatically", "AddNewItem": "Add New Item", + "AddRootFolder": "Add Root Folder", "AddedAuthorSettings": "Added Author Settings", "AddingTag": "Adding tag", "AgeWhenGrabbed": "Age (when grabbed)", diff --git a/src/Readarr.Api.V1/Author/AuthorController.cs b/src/Readarr.Api.V1/Author/AuthorController.cs index e39cb53ca..b1387557b 100644 --- a/src/Readarr.Api.V1/Author/AuthorController.cs +++ b/src/Readarr.Api.V1/Author/AuthorController.cs @@ -59,7 +59,8 @@ public AuthorController(IBroadcastSignalRMessage signalRBroadcaster, AuthorAncestorValidator authorAncestorValidator, SystemFolderValidator systemFolderValidator, QualityProfileExistsValidator qualityProfileExistsValidator, - MetadataProfileExistsValidator metadataProfileExistsValidator) + MetadataProfileExistsValidator metadataProfileExistsValidator, + AuthorFolderAsRootFolderValidator authorFolderAsRootFolderValidator) : base(signalRBroadcaster) { _authorService = authorService; @@ -89,7 +90,10 @@ public AuthorController(IBroadcastSignalRMessage signalRBroadcaster, SharedValidator.RuleFor(s => s.MetadataProfileId).SetValidator(metadataProfileExistsValidator); PostValidator.RuleFor(s => s.Path).IsValidPath().When(s => s.RootFolderPath.IsNullOrWhiteSpace()); - PostValidator.RuleFor(s => s.RootFolderPath).IsValidPath().When(s => s.Path.IsNullOrWhiteSpace()); + PostValidator.RuleFor(s => s.RootFolderPath) + .IsValidPath() + .SetValidator(authorFolderAsRootFolderValidator) + .When(s => s.Path.IsNullOrWhiteSpace()); PostValidator.RuleFor(s => s.AuthorName).NotEmpty(); PostValidator.RuleFor(s => s.ForeignAuthorId).NotEmpty().SetValidator(authorExistsValidator); diff --git a/src/Readarr.Api.V1/Author/AuthorFolderAsRootFolderValidator.cs b/src/Readarr.Api.V1/Author/AuthorFolderAsRootFolderValidator.cs new file mode 100644 index 000000000..f835decef --- /dev/null +++ b/src/Readarr.Api.V1/Author/AuthorFolderAsRootFolderValidator.cs @@ -0,0 +1,49 @@ +using System; +using System.IO; +using FluentValidation.Validators; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Organizer; + +namespace Readarr.Api.V1.Author +{ + public class AuthorFolderAsRootFolderValidator : PropertyValidator + { + private readonly IBuildFileNames _fileNameBuilder; + + public AuthorFolderAsRootFolderValidator(IBuildFileNames fileNameBuilder) + { + _fileNameBuilder = fileNameBuilder; + } + + protected override string GetDefaultMessageTemplate() => "Root folder path contains author folder"; + + protected override bool IsValid(PropertyValidatorContext context) + { + if (context.PropertyValue == null) + { + return true; + } + + var authorResource = context.ParentContext.InstanceToValidate as AuthorResource; + + if (authorResource == null) + { + return true; + } + + var rootFolderPath = context.PropertyValue.ToString(); + var rootFolder = new DirectoryInfo(rootFolderPath).Name; + var author = authorResource.ToModel(); + var authorFolder = _fileNameBuilder.GetAuthorFolder(author); + + if (authorFolder == rootFolder) + { + return false; + } + + var distance = authorFolder.LevenshteinDistance(rootFolder); + + return distance >= Math.Max(1, authorFolder.Length * 0.2); + } + } +} diff --git a/src/Readarr.Api.V1/Author/AuthorLookupController.cs b/src/Readarr.Api.V1/Author/AuthorLookupController.cs index c71210c6c..a6a5e5b09 100644 --- a/src/Readarr.Api.V1/Author/AuthorLookupController.cs +++ b/src/Readarr.Api.V1/Author/AuthorLookupController.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MetadataSource; +using NzbDrone.Core.Organizer; using Readarr.Http; namespace Readarr.Api.V1.Author @@ -11,11 +12,13 @@ namespace Readarr.Api.V1.Author public class AuthorLookupController : Controller { private readonly ISearchForNewAuthor _searchProxy; + private readonly IBuildFileNames _fileNameBuilder; private readonly IMapCoversToLocal _coverMapper; - public AuthorLookupController(ISearchForNewAuthor searchProxy, IMapCoversToLocal coverMapper) + public AuthorLookupController(ISearchForNewAuthor searchProxy, IBuildFileNames fileNameBuilder, IMapCoversToLocal coverMapper) { _searchProxy = searchProxy; + _fileNameBuilder = fileNameBuilder; _coverMapper = coverMapper; } @@ -41,6 +44,8 @@ private IEnumerable MapToResource(IEnumerable Genres { get; set; } public string CleanName { get; set; } public string SortName { get; set; } diff --git a/src/Readarr.Api.V1/RootFolders/RootFolderResource.cs b/src/Readarr.Api.V1/RootFolders/RootFolderResource.cs index 1b6089ffd..f9526b9a2 100644 --- a/src/Readarr.Api.V1/RootFolders/RootFolderResource.cs +++ b/src/Readarr.Api.V1/RootFolders/RootFolderResource.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Books; using NzbDrone.Core.Books.Calibre; using NzbDrone.Core.RootFolders; @@ -47,7 +48,7 @@ public static RootFolderResource ToResource(this RootFolder model) Id = model.Id, Name = model.Name, - Path = model.Path, + Path = model.Path.GetCleanPath(), DefaultMetadataProfileId = model.DefaultMetadataProfileId, DefaultQualityProfileId = model.DefaultQualityProfileId, DefaultMonitorOption = model.DefaultMonitorOption, diff --git a/src/Readarr.Api.V1/Search/SearchController.cs b/src/Readarr.Api.V1/Search/SearchController.cs index bc8af999b..a1c3d85ef 100644 --- a/src/Readarr.Api.V1/Search/SearchController.cs +++ b/src/Readarr.Api.V1/Search/SearchController.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MetadataSource; +using NzbDrone.Core.Organizer; using Readarr.Api.V1.Author; using Readarr.Api.V1.Books; using Readarr.Http; @@ -14,11 +15,13 @@ namespace Readarr.Api.V1.Search public class SearchController : Controller { private readonly ISearchForNewEntity _searchProxy; + private readonly IBuildFileNames _fileNameBuilder; private readonly IMapCoversToLocal _coverMapper; - public SearchController(ISearchForNewEntity searchProxy, IMapCoversToLocal coverMapper) + public SearchController(ISearchForNewEntity searchProxy, IBuildFileNames fileNameBuilder, IMapCoversToLocal coverMapper) { _searchProxy = searchProxy; + _fileNameBuilder = fileNameBuilder; _coverMapper = coverMapper; } @@ -50,6 +53,8 @@ private IEnumerable MapToResource(IEnumerable results) { resource.Author.RemotePoster = poster.Url; } + + resource.Author.Folder = _fileNameBuilder.GetAuthorFolder(author); } else if (result is NzbDrone.Core.Books.Book book) { @@ -67,6 +72,8 @@ private IEnumerable MapToResource(IEnumerable results) { resource.Book.RemoteCover = cover.Url; } + + resource.Book.Author.Folder = _fileNameBuilder.GetAuthorFolder(book.Author); } else {