From cab92745da8af6daa1490a9b0084ecf737603376 Mon Sep 17 00:00:00 2001 From: ta264 Date: Mon, 18 Jan 2021 21:06:49 +0000 Subject: [PATCH] Fixed: better root folder validation --- frontend/src/Helpers/Props/calibreProfiles.js | 156 ++++++++++++++++++ frontend/src/Helpers/Props/index.js | 2 + .../RootFolder/EditRootFolderModalContent.js | 39 ++++- .../src/Store/Actions/Settings/rootFolders.js | 2 +- .../Books/Calibre/CalibreSettings.cs | 1 + .../Resources/CalibreConversionOptions.cs | 2 +- .../MediaFiles/UpgradeMediaFileService.cs | 2 +- .../RootFolders/RootFolderModule.cs | 30 ++++ .../RootFolders/RootFolderResource.cs | 7 +- 9 files changed, 230 insertions(+), 11 deletions(-) create mode 100644 frontend/src/Helpers/Props/calibreProfiles.js diff --git a/frontend/src/Helpers/Props/calibreProfiles.js b/frontend/src/Helpers/Props/calibreProfiles.js new file mode 100644 index 000000000..67b3eb850 --- /dev/null +++ b/frontend/src/Helpers/Props/calibreProfiles.js @@ -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 + }; +}); diff --git a/frontend/src/Helpers/Props/index.js b/frontend/src/Helpers/Props/index.js index f6ea1e4dd..8e0ef47cd 100644 --- a/frontend/src/Helpers/Props/index.js +++ b/frontend/src/Helpers/Props/index.js @@ -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, diff --git a/frontend/src/Settings/MediaManagement/RootFolder/EditRootFolderModalContent.js b/frontend/src/Settings/MediaManagement/RootFolder/EditRootFolderModalContent.js index c8f9e0351..649642bfe 100644 --- a/frontend/src/Settings/MediaManagement/RootFolder/EditRootFolderModalContent.js +++ b/frontend/src/Settings/MediaManagement/RootFolder/EditRootFolderModalContent.js @@ -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 ( @@ -191,7 +193,20 @@ function EditRootFolderModalContent(props) { - Convert to format + + Convert to format + + } + 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} + /> + - Calibre Output Profile + + Calibre Output Profile + + } + 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} + /> + diff --git a/frontend/src/Store/Actions/Settings/rootFolders.js b/frontend/src/Store/Actions/Settings/rootFolders.js index dc3275eb8..a4d9f921b 100644 --- a/frontend/src/Store/Actions/Settings/rootFolders.js +++ b/frontend/src/Store/Actions/Settings/rootFolders.js @@ -50,7 +50,7 @@ export default { host: 'localhost', port: 8080, useSsl: false, - outputProfile: 0, + outputProfile: 'default', defaultTags: [] }, isSaving: false, diff --git a/src/NzbDrone.Core/Books/Calibre/CalibreSettings.cs b/src/NzbDrone.Core/Books/Calibre/CalibreSettings.cs index 53b3ad684..da5768903 100644 --- a/src/NzbDrone.Core/Books/Calibre/CalibreSettings.cs +++ b/src/NzbDrone.Core/Books/Calibre/CalibreSettings.cs @@ -19,6 +19,7 @@ namespace NzbDrone.Core.Books.Calibre RuleFor(c => c.Password).NotEmpty().When(c => !string.IsNullOrWhiteSpace(c.Username)); RuleFor(c => c.OutputFormat).Must(x => x.Split(',').All(y => Enum.TryParse(y, true, out _))).WithMessage("Invalid output formats"); + RuleFor(c => c.OutputProfile).InclusiveBetween(0, (int)Enum.GetValues(typeof(CalibreProfile)).Cast().Max()); } } diff --git a/src/NzbDrone.Core/Books/Calibre/Resources/CalibreConversionOptions.cs b/src/NzbDrone.Core/Books/Calibre/Resources/CalibreConversionOptions.cs index acae49375..31075ee29 100644 --- a/src/NzbDrone.Core/Books/Calibre/Resources/CalibreConversionOptions.cs +++ b/src/NzbDrone.Core/Books/Calibre/Resources/CalibreConversionOptions.cs @@ -27,7 +27,7 @@ namespace NzbDrone.Core.Books.Calibre public enum CalibreProfile { - Default, + @default, cybookg3, cybook_opus, generic_eink, diff --git a/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs b/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs index d60f9a14a..b1cb9706c 100644 --- a/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs @@ -166,7 +166,7 @@ namespace NzbDrone.Core.MediaFiles 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(); } diff --git a/src/Readarr.Api.V1/RootFolders/RootFolderModule.cs b/src/Readarr.Api.V1/RootFolders/RootFolderModule.cs index 706da7ff8..686342a2b 100644 --- a/src/Readarr.Api.V1/RootFolders/RootFolderModule.cs +++ b/src/Readarr.Api.V1/RootFolders/RootFolderModule.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; 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 @@ namespace Readarr.Api.V1.RootFolders 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 @@ namespace Readarr.Api.V1.RootFolders 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(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 @@ namespace Readarr.Api.V1.RootFolders throw new BadRequestException("Cannot edit root folder path"); } + if (model.IsCalibreLibrary) + { + _calibreProxy.Test(model.CalibreSettings); + } + _rootFolderService.Update(model); } diff --git a/src/Readarr.Api.V1/RootFolders/RootFolderResource.cs b/src/Readarr.Api.V1/RootFolders/RootFolderResource.cs index aa723206b..ad98501ef 100644 --- a/src/Readarr.Api.V1/RootFolders/RootFolderResource.cs +++ b/src/Readarr.Api.V1/RootFolders/RootFolderResource.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using NzbDrone.Core.Books; @@ -23,7 +24,7 @@ namespace Readarr.Api.V1.RootFolders 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 @@ namespace Readarr.Api.V1.RootFolders 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 @@ namespace Readarr.Api.V1.RootFolders Password = resource.Password, Library = resource.Library, OutputFormat = resource.OutputFormat, - OutputProfile = resource.OutputProfile, + OutputProfile = (int)Enum.Parse(typeof(CalibreProfile), resource.OutputProfile, true), UseSsl = resource.UseSsl }; }