Fixed: better root folder validation

This commit is contained in:
ta264 2021-01-18 21:06:49 +00:00
parent 6a79c2f3a1
commit cab92745da
9 changed files with 230 additions and 11 deletions

View file

@ -0,0 +1,156 @@
// This is the output of eg. http://192.168.0.3:8089/conversion/book-data/100
const profileData = {
cybookg3: {
name: 'Cybook G3',
description: 'This profile is intended for the Cybook G3. [Screen size: 600 x 800 pixels]'
},
cybook_opus: {
name: 'Cybook Opus',
description: 'This profile is intended for the Cybook Opus. [Screen size: 590 x 775 pixels]'
},
default: {
name: 'Default Output Profile',
description: 'This profile tries to provide sane defaults and is useful if you want to produce a document intended to be read at a computer or on a range of devices. [Screen size: 1600 x 1200 pixels]'
},
generic_eink: {
name: 'Generic e-ink',
description: 'Suitable for use with any e-ink device [Screen size: 590 x 775 pixels]'
},
generic_eink_hd: {
name: 'Generic e-ink HD',
description: 'Suitable for use with any modern high resolution e-ink device [Screen size: unlimited]'
},
generic_eink_large: {
name: 'Generic e-ink large',
description: 'Suitable for use with any large screen e-ink device [Screen size: 600 x 999 pixels]'
},
hanlinv3: {
name: 'Hanlin V3',
description: 'This profile is intended for the Hanlin V3 and its clones. [Screen size: 584 x 754 pixels]'
},
hanlinv5: {
name: 'Hanlin V5',
description: 'This profile is intended for the Hanlin V5 and its clones. [Screen size: 584 x 754 pixels]'
},
illiad: {
name: 'Illiad',
description: 'This profile is intended for the Irex Illiad. [Screen size: 760 x 925 pixels]'
},
ipad: {
name: 'iPad',
description: 'Intended for the iPad and similar devices with a resolution of 768x1024 [Screen size: 768 x 1024 pixels]'
},
ipad3: {
name: 'iPad 3',
description: 'Intended for the iPad 3 and similar devices with a resolution of 1536x2048 [Screen size: 2048 x 1536 pixels]'
},
irexdr1000: {
name: 'IRex Digital Reader 1000',
description: 'This profile is intended for the IRex Digital Reader 1000. [Screen size: 1024 x 1280 pixels]'
},
irexdr800: {
name: 'IRex Digital Reader 800',
description: 'This profile is intended for the IRex Digital Reader 800. [Screen size: 768 x 1024 pixels]'
},
jetbook5: {
name: 'JetBook 5-inch',
description: 'This profile is intended for the 5-inch JetBook. [Screen size: 480 x 640 pixels]'
},
kindle: {
name: 'Kindle',
description: 'This profile is intended for the Amazon Kindle. [Screen size: 525 x 640 pixels]'
},
kindle_dx: {
name: 'Kindle DX',
description: 'This profile is intended for the Amazon Kindle DX. [Screen size: 744 x 1022 pixels]'
},
kindle_fire: {
name: 'Kindle Fire',
description: 'This profile is intended for the Amazon Kindle Fire. [Screen size: 570 x 1016 pixels]'
},
kindle_oasis: {
name: 'Kindle Oasis',
description: 'This profile is intended for the Amazon Kindle Oasis 2017 and above [Screen size: 1264 x 1680 pixels]'
},
kindle_pw: {
name: 'Kindle PaperWhite',
description: 'This profile is intended for the Amazon Kindle PaperWhite 1 and 2 [Screen size: 658 x 940 pixels]'
},
kindle_pw3: {
name: 'Kindle PaperWhite 3',
description: 'This profile is intended for the Amazon Kindle PaperWhite 3 and above [Screen size: 1072 x 1430 pixels]'
},
kindle_voyage: {
name: 'Kindle Voyage',
description: 'This profile is intended for the Amazon Kindle Voyage [Screen size: 1080 x 1430 pixels]'
},
kobo: {
name: 'Kobo Reader',
description: 'This profile is intended for the Kobo Reader. [Screen size: 536 x 710 pixels]'
},
msreader: {
name: 'Microsoft Reader',
description: 'This profile is intended for the Microsoft Reader. [Screen size: 480 x 652 pixels]'
},
mobipocket: {
name: 'Mobipocket Books',
description: 'This profile is intended for the Mobipocket books. [Screen size: 600 x 800 pixels]'
},
nook: {
name: 'Nook',
description: 'This profile is intended for the B&N Nook. [Screen size: 600 x 730 pixels]'
},
nook_color: {
name: 'Nook Color',
description: 'This profile is intended for the B&N Nook Color. [Screen size: 600 x 900 pixels]'
},
nook_hd_plus: {
name: 'Nook HD+',
description: 'Intended for the Nook HD+ and similar tablet devices with a resolution of 1280x1920 [Screen size: 1280 x 1920 pixels]'
},
pocketbook_900: {
name: 'PocketBook Pro 900',
description: 'This profile is intended for the PocketBook Pro 900 series of devices. [Screen size: 810 x 1180 pixels]'
},
pocketbook_pro_912: {
name: 'PocketBook Pro 912',
description: 'This profile is intended for the PocketBook Pro 912 series of devices. [Screen size: 825 x 1200 pixels]'
},
galaxy: {
name: 'Samsung Galaxy',
description: 'Intended for the Samsung Galaxy and similar tablet devices with a resolution of 600x1280 [Screen size: 600 x 1280 pixels]'
},
sony: {
name: 'Sony Reader',
description: 'This profile is intended for the SONY PRS line. The 500/505/600/700 etc. [Screen size: 590 x 775 pixels]'
},
sony300: {
name: 'Sony Reader 300',
description: 'This profile is intended for the SONY PRS-300. [Screen size: 590 x 775 pixels]'
},
sony900: {
name: 'Sony Reader 900',
description: 'This profile is intended for the SONY PRS-900. [Screen size: 600 x 999 pixels]'
},
sony_landscape: {
name: 'Sony Reader Landscape',
description: 'This profile is intended for the SONY PRS line. The 500/505/700 etc, in landscape mode. Mainly useful for comics. [Screen size: 784 x 1012 pixels]'
},
sonyt3: {
name: 'Sony Reader T3',
description: 'This profile is intended for the SONY PRS-T3. [Screen size: 758 x 934 pixels]'
},
tablet: {
name: 'Tablet',
description: 'Intended for generic tablet devices, does no resizing of images [Screen size: unlimited]'
}
};
export const options = Object.entries(profileData).map(([key, object]) => {
return {
key,
value: object.name,
description: object.description
};
});

View file

@ -1,4 +1,5 @@
import * as align from './align';
import * as calibreProfiles from './calibreProfiles';
import * as filterBuilderTypes from './filterBuilderTypes';
import * as filterBuilderValueTypes from './filterBuilderValueTypes';
import filterTypePredicates from './filterTypePredicates';
@ -15,6 +16,7 @@ import * as tooltipPositions from './tooltipPositions';
export {
align,
calibreProfiles,
inputTypes,
filterBuilderTypes,
filterBuilderValueTypes,

View file

@ -15,7 +15,7 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import Popover from 'Components/Tooltip/Popover';
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
import { calibreProfiles, icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
import styles from './EditRootFolderModalContent.css';
function EditRootFolderModalContent(props) {
@ -55,6 +55,8 @@ function EditRootFolderModalContent(props) {
useSsl
} = item;
const profileHelpText = calibreProfiles.options.find((x) => x.key === outputProfile.value).description;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
@ -191,7 +193,20 @@ function EditRootFolderModalContent(props) {
</FormGroup>
<FormGroup>
<FormLabel>Convert to format</FormLabel>
<FormLabel>
Convert to format
<Popover
anchor={
<Icon
className={styles.labelIcon}
name={icons.INFO}
/>
}
title="Calibre output format"
body={'Specify the output format. Options are: MOBI, EPUB, AZW3, DOCX, FB2, HTMLZ, LIT, LRF, PDB, PDF, PMLZ, RB, RTF, SNB, TCR, TXT, TXTZ, ZIP'}
position={tooltipPositions.RIGHT}
/>
</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
@ -203,12 +218,26 @@ function EditRootFolderModalContent(props) {
</FormGroup>
<FormGroup>
<FormLabel>Calibre Output Profile</FormLabel>
<FormLabel>
Calibre Output Profile
<Popover
anchor={
<Icon
className={styles.labelIcon}
name={icons.INFO}
/>
}
title="Calibre output profile"
body={'Specify the output profile. The output profile tells the calibre conversion system how to optimize the created document for the specified device (such as by resizing images for the device screen size). In some cases, an output profile can be used to optimize the output for a particular device, but this is rarely necessary.'}
position={tooltipPositions.RIGHT}
/>
</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
type={inputTypes.SELECT}
name="outputProfile"
helpText="Output profile for conversion"
values={calibreProfiles.options}
helpText={profileHelpText}
{...outputProfile}
onChange={onInputChange}
/>

View file

@ -50,7 +50,7 @@ export default {
host: 'localhost',
port: 8080,
useSsl: false,
outputProfile: 0,
outputProfile: 'default',
defaultTags: []
},
isSaving: false,

View file

@ -19,6 +19,7 @@ public CalibreSettingsValidator()
RuleFor(c => c.Password).NotEmpty().When(c => !string.IsNullOrWhiteSpace(c.Username));
RuleFor(c => c.OutputFormat).Must(x => x.Split(',').All(y => Enum.TryParse<CalibreFormat>(y, true, out _))).WithMessage("Invalid output formats");
RuleFor(c => c.OutputProfile).InclusiveBetween(0, (int)Enum.GetValues(typeof(CalibreProfile)).Cast<CalibreProfile>().Max());
}
}

View file

@ -27,7 +27,7 @@ public enum CalibreFormat
public enum CalibreProfile
{
Default,
@default,
cybookg3,
cybook_opus,
generic_eink,

View file

@ -166,7 +166,7 @@ public BookFile CalibreAddAndConvert(BookFile file, CalibreSettings settings)
options.Conversion_options.Output_fmt = format;
if (settings.OutputProfile != (int)CalibreProfile.Default)
if (settings.OutputProfile != (int)CalibreProfile.@default)
{
options.Conversion_options.Options.Output_profile = ((CalibreProfile)settings.OutputProfile).ToString();
}

View file

@ -3,6 +3,7 @@
using System.Linq;
using FluentValidation;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Books.Calibre;
using NzbDrone.Core.RootFolders;
using NzbDrone.Core.Validation;
@ -52,6 +53,11 @@ public RootFolderModule(IRootFolderService rootFolderService,
PostValidator.RuleFor(c => c.Path)
.SetValidator(rootFolderValidator);
SharedValidator.RuleFor(c => c)
.Must(x => CalibreLibraryOnlyUsedOnce(x))
.When(x => x.IsCalibreLibrary)
.WithMessage("Calibre library is already configured as a root folder");
SharedValidator.RuleFor(c => c.Name)
.NotEmpty();
@ -68,6 +74,25 @@ public RootFolderModule(IRootFolderService rootFolderService,
SharedValidator.RuleFor(c => c.Password).NotEmpty().When(c => !string.IsNullOrWhiteSpace(c.Username));
SharedValidator.RuleFor(c => c.OutputFormat).Must(x => x.Split(',').All(y => Enum.TryParse<CalibreFormat>(y, true, out _))).When(x => x.OutputFormat.IsNotNullOrWhiteSpace()).WithMessage("Invalid output formats");
SharedValidator.RuleFor(c => c.OutputProfile).IsEnumName(typeof(CalibreProfile));
}
private bool CalibreLibraryOnlyUsedOnce(RootFolderResource settings)
{
var newUri = GetLibraryUri(settings);
return !_rootFolderService.All().Exists(x => x.Id != settings.Id &&
x.CalibreSettings != null &&
GetLibraryUri(x.CalibreSettings) == newUri);
}
private string GetLibraryUri(RootFolderResource settings)
{
return HttpUri.CombinePath(HttpRequestBuilder.BuildBaseUrl(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase), settings.Library);
}
private string GetLibraryUri(CalibreSettings settings)
{
return HttpUri.CombinePath(HttpRequestBuilder.BuildBaseUrl(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase), settings.Library);
}
private RootFolderResource GetRootFolder(int id)
@ -96,6 +121,11 @@ private void UpdateRootFolder(RootFolderResource rootFolderResource)
throw new BadRequestException("Cannot edit root folder path");
}
if (model.IsCalibreLibrary)
{
_calibreProxy.Test(model.CalibreSettings);
}
_rootFolderService.Update(model);
}

View file

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Core.Books;
@ -23,7 +24,7 @@ public class RootFolderResource : RestResource
public string Password { get; set; }
public string Library { get; set; }
public string OutputFormat { get; set; }
public int OutputProfile { get; set; }
public string OutputProfile { get; set; }
public bool UseSsl { get; set; }
public bool Accessible { get; set; }
@ -58,7 +59,7 @@ public static RootFolderResource ToResource(this RootFolder model)
Password = model.CalibreSettings?.Password,
Library = model.CalibreSettings?.Library,
OutputFormat = model.CalibreSettings?.OutputFormat,
OutputProfile = model.CalibreSettings?.OutputProfile ?? 0,
OutputProfile = ((CalibreProfile)(model.CalibreSettings?.OutputProfile ?? 0)).ToString(),
UseSsl = model.CalibreSettings?.UseSsl ?? false,
Accessible = model.Accessible,
@ -86,7 +87,7 @@ public static RootFolder ToModel(this RootFolderResource resource)
Password = resource.Password,
Library = resource.Library,
OutputFormat = resource.OutputFormat,
OutputProfile = resource.OutputProfile,
OutputProfile = (int)Enum.Parse(typeof(CalibreProfile), resource.OutputProfile, true),
UseSsl = resource.UseSsl
};
}