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)
This commit is contained in:
Mark McDowall 2019-08-03 18:55:31 -07:00 committed by Bogdan
parent 0972d41bf8
commit f819e582cf
26 changed files with 225 additions and 29 deletions

View file

@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Button from 'Components/Link/Button'; import Button from 'Components/Link/Button';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './NoAuthor.css'; import styles from './NoAuthor.css';
function NoAuthor(props) { function NoAuthor(props) {
@ -31,7 +32,7 @@ function NoAuthor(props) {
to="/settings/mediamanagement" to="/settings/mediamanagement"
kind={kinds.PRIMARY} kind={kinds.PRIMARY}
> >
Add Root Folder {translate('AddRootFolder')}
</Button> </Button>
</div> </div>
@ -40,7 +41,7 @@ function NoAuthor(props) {
to="/add/search" to="/add/search"
kind={kinds.PRIMARY} kind={kinds.PRIMARY}
> >
Add New Author {translate('AddNewAuthor')}
</Button> </Button>
</div> </div>
</div> </div>

View file

@ -28,8 +28,7 @@ function createMapStateToProps() {
if (includeNoChange) { if (includeNoChange) {
values.unshift({ values.unshift({
key: 'noChange', key: 'noChange',
value: '', value: translate('NoChange'),
name: translate('NoChange'),
isDisabled: includeNoChangeDisabled, isDisabled: includeNoChangeDisabled,
isMissing: false isMissing: false
}); });
@ -39,7 +38,6 @@ function createMapStateToProps() {
values.push({ values.push({
key: '', key: '',
value: '', value: '',
name: '',
isDisabled: true, isDisabled: true,
isHidden: true isHidden: true
}); });
@ -56,8 +54,7 @@ function createMapStateToProps() {
values.push({ values.push({
key: ADD_NEW_KEY, key: ADD_NEW_KEY,
value: '', value: 'Add a new path'
name: 'Add a new path'
}); });
return { return {

View file

@ -13,6 +13,15 @@
} }
} }
.value {
display: flex;
}
.authorFolder {
flex: 0 0 auto;
color: var(--disabledColor);
}
.freeSpace { .freeSpace {
margin-left: 15px; margin-left: 15px;
color: var(--darkGray); color: var(--darkGray);

View file

@ -1,10 +1,12 @@
// This file is automatically generated. // This file is automatically generated.
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'authorFolder': string;
'freeSpace': string; 'freeSpace': string;
'isMissing': string; 'isMissing': string;
'isMobile': string; 'isMobile': string;
'optionText': string; 'optionText': string;
'value': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
export default cssExports; export default cssExports;

View file

@ -7,18 +7,24 @@ import styles from './RootFolderSelectInputOption.css';
function RootFolderSelectInputOption(props) { function RootFolderSelectInputOption(props) {
const { const {
id,
value, value,
name, name,
freeSpace, freeSpace,
authorFolder,
isMissing, isMissing,
isMobile, isMobile,
isWindows,
...otherProps ...otherProps
} = props; } = props;
const text = value === '' ? name : `${name} [${value}]`; const slashCharacter = isWindows ? '\\' : '/';
const text = name === '' ? value : `[${name}] ${value}`;
return ( return (
<EnhancedSelectInputOption <EnhancedSelectInputOption
id={id}
isMobile={isMobile} isMobile={isMobile}
{...otherProps} {...otherProps}
> >
@ -27,7 +33,18 @@ function RootFolderSelectInputOption(props) {
isMobile && styles.isMobile isMobile && styles.isMobile
)} )}
> >
<div>{text}</div> <div className={styles.value}>
{text}
{
authorFolder && id !== 'addNew' ?
<div className={styles.authorFolder}>
{slashCharacter}
{authorFolder}
</div> :
null
}
</div>
{ {
freeSpace == null ? freeSpace == null ?
@ -50,11 +67,18 @@ function RootFolderSelectInputOption(props) {
} }
RootFolderSelectInputOption.propTypes = { RootFolderSelectInputOption.propTypes = {
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
value: PropTypes.string.isRequired, value: PropTypes.string.isRequired,
freeSpace: PropTypes.number, freeSpace: PropTypes.number,
authorFolder: PropTypes.string,
isMissing: PropTypes.bool, isMissing: PropTypes.bool,
isMobile: PropTypes.bool.isRequired isMobile: PropTypes.bool.isRequired,
isWindows: PropTypes.bool
};
RootFolderSelectInputOption.defaultProps = {
name: ''
}; };
export default RootFolderSelectInputOption; export default RootFolderSelectInputOption;

View file

@ -7,10 +7,20 @@
overflow: hidden; overflow: hidden;
} }
.pathContainer {
@add-mixin truncate;
display: flex;
flex: 1 0 0;
}
.path { .path {
@add-mixin truncate; @add-mixin truncate;
flex: 0 1 auto;
}
flex: 1 0 0; .authorFolder {
flex: 0 1 auto;
color: var(--disabledColor);
} }
.freeSpace { .freeSpace {

View file

@ -1,8 +1,10 @@
// This file is automatically generated. // This file is automatically generated.
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'authorFolder': string;
'freeSpace': string; 'freeSpace': string;
'path': string; 'path': string;
'pathContainer': string;
'selectedValue': string; 'selectedValue': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;

View file

@ -9,19 +9,34 @@ function RootFolderSelectInputSelectedValue(props) {
name, name,
value, value,
freeSpace, freeSpace,
authorFolder,
includeFreeSpace, includeFreeSpace,
isWindows,
...otherProps ...otherProps
} = props; } = props;
const text = value === '' ? name : `${name} [${value}]`; const slashCharacter = isWindows ? '\\' : '/';
const text = name === '' ? value : `[${name}] ${value}`;
return ( return (
<EnhancedSelectInputSelectedValue <EnhancedSelectInputSelectedValue
className={styles.selectedValue} className={styles.selectedValue}
{...otherProps} {...otherProps}
> >
<div className={styles.path}> <div className={styles.pathContainer}>
{text} <div className={styles.path}>
{text}
</div>
{
authorFolder ?
<div className={styles.authorFolder}>
{slashCharacter}
{authorFolder}
</div> :
null
}
</div> </div>
{ {
@ -38,10 +53,13 @@ RootFolderSelectInputSelectedValue.propTypes = {
name: PropTypes.string, name: PropTypes.string,
value: PropTypes.string, value: PropTypes.string,
freeSpace: PropTypes.number, freeSpace: PropTypes.number,
authorFolder: PropTypes.string,
isWindows: PropTypes.bool,
includeFreeSpace: PropTypes.bool.isRequired includeFreeSpace: PropTypes.bool.isRequired
}; };
RootFolderSelectInputSelectedValue.defaultProps = { RootFolderSelectInputSelectedValue.defaultProps = {
name: '',
includeFreeSpace: true includeFreeSpace: true
}; };

View file

@ -35,11 +35,12 @@
.message { .message {
margin-top: 30px; margin-top: 30px;
text-align: center; text-align: center;
font-weight: 300;
font-size: $largeFontSize;
} }
.helpText { .helpText {
margin-bottom: 10px; margin-bottom: 10px;
font-weight: 300;
font-size: 24px; font-size: 24px;
} }

View file

@ -82,7 +82,8 @@ class AddNewItem extends Component {
render() { render() {
const { const {
error, error,
items items,
hasExistingAuthors
} = this.props; } = this.props;
const term = this.state.term; const term = this.state.term;
@ -186,7 +187,8 @@ class AddNewItem extends Component {
} }
{ {
!term && term ?
null :
<div className={styles.message}> <div className={styles.message}>
<div className={styles.helpText}> <div className={styles.helpText}>
{translate('ItsEasyToAddANewAuthorOrBookJustStartTypingTheNameOfTheItemYouWantToAdd')} {translate('ItsEasyToAddANewAuthorOrBookJustStartTypingTheNameOfTheItemYouWantToAdd')}
@ -199,6 +201,24 @@ class AddNewItem extends Component {
</div> </div>
} }
{
!term && !hasExistingAuthors ?
<div className={styles.message}>
<div className={styles.noAuthorsText}>
You haven't added any authors yet, do you want to add an existing library location (Root Folder) and update?
</div>
<div>
<Button
to="/settings/mediamanagement"
kind={kinds.PRIMARY}
>
{translate('AddRootFolder')}
</Button>
</div>
</div> :
null
}
<div /> <div />
</PageContentBody> </PageContentBody>
</PageContent> </PageContent>
@ -213,6 +233,7 @@ AddNewItem.propTypes = {
isAdding: PropTypes.bool.isRequired, isAdding: PropTypes.bool.isRequired,
addError: PropTypes.object, addError: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
hasExistingAuthors: PropTypes.bool.isRequired,
onSearchChange: PropTypes.func.isRequired, onSearchChange: PropTypes.func.isRequired,
onClearSearch: PropTypes.func.isRequired onClearSearch: PropTypes.func.isRequired
}; };

View file

@ -10,13 +10,15 @@ import AddNewItem from './AddNewItem';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.search, (state) => state.search,
(state) => state.authors.items.length,
(state) => state.router.location, (state) => state.router.location,
(search, location) => { (search, existingAuthorsCount, location) => {
const { params } = parseUrl(location.search); const { params } = parseUrl(location.search);
return { return {
...search,
term: params.term, term: params.term,
...search hasExistingAuthors: existingAuthorsCount > 0
}; };
} }
); );

View file

@ -9,6 +9,7 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import AddAuthorOptionsForm from '../Common/AddAuthorOptionsForm.js'; import AddAuthorOptionsForm from '../Common/AddAuthorOptionsForm.js';
import styles from './AddNewAuthorModalContent.css'; import styles from './AddNewAuthorModalContent.css';
@ -54,7 +55,7 @@ class AddNewAuthorModalContent extends Component {
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
Add new Author {translate('AddNewAuthor')}
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
@ -133,7 +134,7 @@ class AddNewAuthorModalContent extends Component {
AddNewAuthorModalContent.propTypes = { AddNewAuthorModalContent.propTypes = {
authorName: PropTypes.string.isRequired, authorName: PropTypes.string.isRequired,
disambiguation: PropTypes.string.isRequired, disambiguation: PropTypes.string,
overview: PropTypes.string, overview: PropTypes.string,
images: PropTypes.arrayOf(PropTypes.object).isRequired, images: PropTypes.arrayOf(PropTypes.object).isRequired,
isAdding: PropTypes.bool.isRequired, isAdding: PropTypes.bool.isRequired,

View file

@ -4,6 +4,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { addAuthor, setAuthorAddDefault } from 'Store/Actions/searchActions'; import { addAuthor, setAuthorAddDefault } from 'Store/Actions/searchActions';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
import selectSettings from 'Store/Selectors/selectSettings'; import selectSettings from 'Store/Selectors/selectSettings';
import AddNewAuthorModalContent from './AddNewAuthorModalContent'; import AddNewAuthorModalContent from './AddNewAuthorModalContent';
@ -12,7 +13,8 @@ function createMapStateToProps() {
(state) => state.search, (state) => state.search,
(state) => state.settings.metadataProfiles, (state) => state.settings.metadataProfiles,
createDimensionsSelector(), createDimensionsSelector(),
(searchState, metadataProfiles, dimensions) => { createSystemStatusSelector(),
(searchState, metadataProfiles, dimensions, systemStatus) => {
const { const {
isAdding, isAdding,
addError, addError,
@ -32,6 +34,7 @@ function createMapStateToProps() {
isSmallScreen: dimensions.isSmallScreen, isSmallScreen: dimensions.isSmallScreen,
validationErrors, validationErrors,
validationWarnings, validationWarnings,
isWindows: systemStatus.isWindows,
...settings ...settings
}; };
} }

View file

@ -78,6 +78,7 @@ class AddNewAuthorSearchResult extends Component {
status, status,
overview, overview,
ratings, ratings,
folder,
images, images,
isExistingAuthor, isExistingAuthor,
isSmallScreen isSmallScreen
@ -205,6 +206,7 @@ class AddNewAuthorSearchResult extends Component {
disambiguation={disambiguation} disambiguation={disambiguation}
year={year} year={year}
overview={overview} overview={overview}
folder={folder}
images={images} images={images}
onModalClose={this.onAddAuthorModalClose} onModalClose={this.onAddAuthorModalClose}
/> />
@ -222,6 +224,7 @@ AddNewAuthorSearchResult.propTypes = {
status: PropTypes.string.isRequired, status: PropTypes.string.isRequired,
overview: PropTypes.string, overview: PropTypes.string,
ratings: PropTypes.object.isRequired, ratings: PropTypes.object.isRequired,
folder: PropTypes.string.isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired, images: PropTypes.arrayOf(PropTypes.object).isRequired,
isExistingAuthor: PropTypes.bool.isRequired, isExistingAuthor: PropTypes.bool.isRequired,
isSmallScreen: PropTypes.bool.isRequired isSmallScreen: PropTypes.bool.isRequired

View file

@ -10,6 +10,7 @@ import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import stripHtml from 'Utilities/String/stripHtml'; import stripHtml from 'Utilities/String/stripHtml';
import translate from 'Utilities/String/translate';
import AddAuthorOptionsForm from '../Common/AddAuthorOptionsForm.js'; import AddAuthorOptionsForm from '../Common/AddAuthorOptionsForm.js';
import styles from './AddNewBookModalContent.css'; import styles from './AddNewBookModalContent.css';
@ -58,7 +59,7 @@ class AddNewBookModalContent extends Component {
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
Add new Book {translate('AddNewBook')}
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>

View file

@ -4,6 +4,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { addBook, setBookAddDefault } from 'Store/Actions/searchActions'; import { addBook, setBookAddDefault } from 'Store/Actions/searchActions';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
import selectSettings from 'Store/Selectors/selectSettings'; import selectSettings from 'Store/Selectors/selectSettings';
import AddNewBookModalContent from './AddNewBookModalContent'; import AddNewBookModalContent from './AddNewBookModalContent';
@ -13,7 +14,8 @@ function createMapStateToProps() {
(state) => state.search, (state) => state.search,
(state) => state.settings.metadataProfiles, (state) => state.settings.metadataProfiles,
createDimensionsSelector(), createDimensionsSelector(),
(isExistingAuthor, searchState, metadataProfiles, dimensions) => { createSystemStatusSelector(),
(isExistingAuthor, searchState, metadataProfiles, dimensions, systemStatus) => {
const { const {
isAdding, isAdding,
addError, addError,
@ -33,6 +35,7 @@ function createMapStateToProps() {
isSmallScreen: dimensions.isSmallScreen, isSmallScreen: dimensions.isSmallScreen,
validationErrors, validationErrors,
validationWarnings, validationWarnings,
isWindows: systemStatus.isWindows,
...settings ...settings
}; };
} }

View file

@ -203,6 +203,7 @@ class AddNewBookSearchResult extends Component {
disambiguation={disambiguation} disambiguation={disambiguation}
authorName={author.authorName} authorName={author.authorName}
overview={overview} overview={overview}
folder={author.folder}
images={images} images={images}
onModalClose={this.onAddBookModalClose} onModalClose={this.onAddBookModalClose}
/> />

View file

@ -39,7 +39,9 @@ class AddAuthorOptionsForm extends Component {
includeNoneMetadataProfile, includeNoneMetadataProfile,
includeSpecificBookMonitor, includeSpecificBookMonitor,
showMetadataProfile, showMetadataProfile,
folder,
tags, tags,
isWindows,
onInputChange, onInputChange,
...otherProps ...otherProps
} = this.props; } = this.props;
@ -54,6 +56,15 @@ class AddAuthorOptionsForm extends Component {
<FormInputGroup <FormInputGroup
type={inputTypes.ROOT_FOLDER_SELECT} type={inputTypes.ROOT_FOLDER_SELECT}
name="rootFolderPath" name="rootFolderPath"
valueOptions={{
authorFolder: folder,
isWindows
}}
selectedValueOptions={{
authorFolder: folder,
isWindows
}}
helpText={translate('AddNewAuthorRootFolderHelpText', { folder })}
onChange={onInputChange} onChange={onInputChange}
{...rootFolderPath} {...rootFolderPath}
/> />
@ -179,8 +190,14 @@ AddAuthorOptionsForm.propTypes = {
showMetadataProfile: PropTypes.bool.isRequired, showMetadataProfile: PropTypes.bool.isRequired,
includeNoneMetadataProfile: PropTypes.bool.isRequired, includeNoneMetadataProfile: PropTypes.bool.isRequired,
includeSpecificBookMonitor: PropTypes.bool.isRequired, includeSpecificBookMonitor: PropTypes.bool.isRequired,
folder: PropTypes.string.isRequired,
tags: PropTypes.object.isRequired, tags: PropTypes.object.isRequired,
isWindows: PropTypes.bool.isRequired,
onInputChange: PropTypes.func.isRequired onInputChange: PropTypes.func.isRequired
}; };
AddAuthorOptionsForm.defaultProps = {
includeSpecificBookMonitor: false
};
export default AddAuthorOptionsForm; export default AddAuthorOptionsForm;

View file

@ -108,6 +108,15 @@ public static string GetParentPath(this string childPath)
return Directory.GetParent(cleanPath)?.FullName; 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) public static bool IsParentPath(this string parentPath, string childPath)
{ {
if (parentPath != "/" && !parentPath.EndsWith(":\\")) if (parentPath != "/" && !parentPath.EndsWith(":\\"))

View file

@ -11,7 +11,11 @@
"AddListExclusion": "Add List Exclusion", "AddListExclusion": "Add List Exclusion",
"AddMissing": "Add missing", "AddMissing": "Add missing",
"AddNew": "Add New", "AddNew": "Add New",
"AddNewBook": "Add New Book",
"AddNewAuthor": "Add New Author",
"AddNewAuthorRootFolderHelpText": "'{folder}' subfolder will be created automatically",
"AddNewItem": "Add New Item", "AddNewItem": "Add New Item",
"AddRootFolder": "Add Root Folder",
"AddedAuthorSettings": "Added Author Settings", "AddedAuthorSettings": "Added Author Settings",
"AddingTag": "Adding tag", "AddingTag": "Adding tag",
"AgeWhenGrabbed": "Age (when grabbed)", "AgeWhenGrabbed": "Age (when grabbed)",

View file

@ -59,7 +59,8 @@ public AuthorController(IBroadcastSignalRMessage signalRBroadcaster,
AuthorAncestorValidator authorAncestorValidator, AuthorAncestorValidator authorAncestorValidator,
SystemFolderValidator systemFolderValidator, SystemFolderValidator systemFolderValidator,
QualityProfileExistsValidator qualityProfileExistsValidator, QualityProfileExistsValidator qualityProfileExistsValidator,
MetadataProfileExistsValidator metadataProfileExistsValidator) MetadataProfileExistsValidator metadataProfileExistsValidator,
AuthorFolderAsRootFolderValidator authorFolderAsRootFolderValidator)
: base(signalRBroadcaster) : base(signalRBroadcaster)
{ {
_authorService = authorService; _authorService = authorService;
@ -89,7 +90,10 @@ public AuthorController(IBroadcastSignalRMessage signalRBroadcaster,
SharedValidator.RuleFor(s => s.MetadataProfileId).SetValidator(metadataProfileExistsValidator); SharedValidator.RuleFor(s => s.MetadataProfileId).SetValidator(metadataProfileExistsValidator);
PostValidator.RuleFor(s => s.Path).IsValidPath().When(s => s.RootFolderPath.IsNullOrWhiteSpace()); 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.AuthorName).NotEmpty();
PostValidator.RuleFor(s => s.ForeignAuthorId).NotEmpty().SetValidator(authorExistsValidator); PostValidator.RuleFor(s => s.ForeignAuthorId).NotEmpty().SetValidator(authorExistsValidator);

View file

@ -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);
}
}
}

View file

@ -3,6 +3,7 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MetadataSource; using NzbDrone.Core.MetadataSource;
using NzbDrone.Core.Organizer;
using Readarr.Http; using Readarr.Http;
namespace Readarr.Api.V1.Author namespace Readarr.Api.V1.Author
@ -11,11 +12,13 @@ namespace Readarr.Api.V1.Author
public class AuthorLookupController : Controller public class AuthorLookupController : Controller
{ {
private readonly ISearchForNewAuthor _searchProxy; private readonly ISearchForNewAuthor _searchProxy;
private readonly IBuildFileNames _fileNameBuilder;
private readonly IMapCoversToLocal _coverMapper; private readonly IMapCoversToLocal _coverMapper;
public AuthorLookupController(ISearchForNewAuthor searchProxy, IMapCoversToLocal coverMapper) public AuthorLookupController(ISearchForNewAuthor searchProxy, IBuildFileNames fileNameBuilder, IMapCoversToLocal coverMapper)
{ {
_searchProxy = searchProxy; _searchProxy = searchProxy;
_fileNameBuilder = fileNameBuilder;
_coverMapper = coverMapper; _coverMapper = coverMapper;
} }
@ -41,6 +44,8 @@ private IEnumerable<AuthorResource> MapToResource(IEnumerable<NzbDrone.Core.Book
resource.RemotePoster = poster.Url; resource.RemotePoster = poster.Url;
} }
resource.Folder = _fileNameBuilder.GetAuthorFolder(currentAuthor);
yield return resource; yield return resource;
} }
} }

View file

@ -45,6 +45,7 @@ public class AuthorResource : RestResource
public NewItemMonitorTypes MonitorNewItems { get; set; } public NewItemMonitorTypes MonitorNewItems { get; set; }
public string RootFolderPath { get; set; } public string RootFolderPath { get; set; }
public string Folder { get; set; }
public List<string> Genres { get; set; } public List<string> Genres { get; set; }
public string CleanName { get; set; } public string CleanName { get; set; }
public string SortName { get; set; } public string SortName { get; set; }

View file

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Books; using NzbDrone.Core.Books;
using NzbDrone.Core.Books.Calibre; using NzbDrone.Core.Books.Calibre;
using NzbDrone.Core.RootFolders; using NzbDrone.Core.RootFolders;
@ -47,7 +48,7 @@ public static RootFolderResource ToResource(this RootFolder model)
Id = model.Id, Id = model.Id,
Name = model.Name, Name = model.Name,
Path = model.Path, Path = model.Path.GetCleanPath(),
DefaultMetadataProfileId = model.DefaultMetadataProfileId, DefaultMetadataProfileId = model.DefaultMetadataProfileId,
DefaultQualityProfileId = model.DefaultQualityProfileId, DefaultQualityProfileId = model.DefaultQualityProfileId,
DefaultMonitorOption = model.DefaultMonitorOption, DefaultMonitorOption = model.DefaultMonitorOption,

View file

@ -4,6 +4,7 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MetadataSource; using NzbDrone.Core.MetadataSource;
using NzbDrone.Core.Organizer;
using Readarr.Api.V1.Author; using Readarr.Api.V1.Author;
using Readarr.Api.V1.Books; using Readarr.Api.V1.Books;
using Readarr.Http; using Readarr.Http;
@ -14,11 +15,13 @@ namespace Readarr.Api.V1.Search
public class SearchController : Controller public class SearchController : Controller
{ {
private readonly ISearchForNewEntity _searchProxy; private readonly ISearchForNewEntity _searchProxy;
private readonly IBuildFileNames _fileNameBuilder;
private readonly IMapCoversToLocal _coverMapper; private readonly IMapCoversToLocal _coverMapper;
public SearchController(ISearchForNewEntity searchProxy, IMapCoversToLocal coverMapper) public SearchController(ISearchForNewEntity searchProxy, IBuildFileNames fileNameBuilder, IMapCoversToLocal coverMapper)
{ {
_searchProxy = searchProxy; _searchProxy = searchProxy;
_fileNameBuilder = fileNameBuilder;
_coverMapper = coverMapper; _coverMapper = coverMapper;
} }
@ -50,6 +53,8 @@ private IEnumerable<SearchResource> MapToResource(IEnumerable<object> results)
{ {
resource.Author.RemotePoster = poster.Url; resource.Author.RemotePoster = poster.Url;
} }
resource.Author.Folder = _fileNameBuilder.GetAuthorFolder(author);
} }
else if (result is NzbDrone.Core.Books.Book book) else if (result is NzbDrone.Core.Books.Book book)
{ {
@ -67,6 +72,8 @@ private IEnumerable<SearchResource> MapToResource(IEnumerable<object> results)
{ {
resource.Book.RemoteCover = cover.Url; resource.Book.RemoteCover = cover.Url;
} }
resource.Book.Author.Folder = _fileNameBuilder.GetAuthorFolder(book.Author);
} }
else else
{ {