diff --git a/frontend/src/Settings/MediaManagement/RootFolder/EditRootFolderModalContent.js b/frontend/src/Settings/MediaManagement/RootFolder/EditRootFolderModalContent.js
index 05339e04f..c8f9e0351 100644
--- a/frontend/src/Settings/MediaManagement/RootFolder/EditRootFolderModalContent.js
+++ b/frontend/src/Settings/MediaManagement/RootFolder/EditRootFolderModalContent.js
@@ -49,6 +49,7 @@ function EditRootFolderModalContent(props) {
urlBase,
username,
password,
+ library,
outputFormat,
outputProfile,
useSsl
@@ -100,7 +101,7 @@ function EditRootFolderModalContent(props) {
- Calibre Library
+ Use Calibre
+
+ Calibre Library
+
+
+
+
Convert to format
diff --git a/src/NzbDrone.Core/Books/Calibre/CalibreProxy.cs b/src/NzbDrone.Core/Books/Calibre/CalibreProxy.cs
index 883536764..25c85c4ed 100644
--- a/src/NzbDrone.Core/Books/Calibre/CalibreProxy.cs
+++ b/src/NzbDrone.Core/Books/Calibre/CalibreProxy.cs
@@ -4,6 +4,8 @@
using System.Linq;
using System.Net;
using System.Threading.Tasks;
+using FluentValidation;
+using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Disk;
@@ -13,12 +15,12 @@
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.RemotePathMappings;
+using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Books.Calibre
{
public interface ICalibreProxy
{
- void GetLibraryInfo(CalibreSettings settings);
CalibreImportJob AddBook(BookFile book, CalibreSettings settings);
void AddFormat(BookFile file, CalibreSettings settings);
void RemoveFormats(int calibreId, IEnumerable formats, CalibreSettings settings);
@@ -27,10 +29,13 @@ public interface ICalibreProxy
long ConvertBook(int calibreId, CalibreConversionOptions options, CalibreSettings settings);
List GetAllBookFilePaths(CalibreSettings settings);
CalibreBook GetBook(int calibreId, CalibreSettings settings);
+ void Test(CalibreSettings settings);
}
public class CalibreProxy : ICalibreProxy
{
+ private const int PAGE_SIZE = 1000;
+
private readonly IHttpClient _httpClient;
private readonly IMapCoversToLocal _mediaCoverService;
private readonly IRemotePathMappingService _pathMapper;
@@ -62,7 +67,7 @@ public CalibreImportJob AddBook(BookFile book, CalibreSettings settings)
try
{
- var builder = GetBuilder($"cdb/add-book/{jobid}/{addDuplicates}/{filename}", settings);
+ var builder = GetBuilder($"cdb/add-book/{jobid}/{addDuplicates}/{filename}/{settings.Library}", settings);
var request = builder.Build();
request.SetContent(body);
@@ -171,7 +176,7 @@ public void SetFields(BookFile file, CalibreSettings settings)
private void ExecuteSetFields(int id, CalibreChangesPayload payload, CalibreSettings settings)
{
- var builder = GetBuilder($"cdb/set-fields/{id}", settings)
+ var builder = GetBuilder($"cdb/set-fields/{id}/{settings.Library}", settings)
.Post()
.SetHeader("Content-Type", "application/json");
@@ -185,9 +190,9 @@ public CalibreBookData GetBookData(int calibreId, CalibreSettings settings)
{
try
{
- var builder = GetBuilder($"conversion/book-data/{calibreId}", settings);
-
- var request = builder.Build();
+ var request = GetBuilder($"conversion/book-data/{calibreId}", settings)
+ .AddQueryParam("library_id", settings.Library)
+ .Build();
return _httpClient.Get(request).Resource;
}
@@ -201,9 +206,9 @@ public long ConvertBook(int calibreId, CalibreConversionOptions options, Calibre
{
try
{
- var builder = GetBuilder($"conversion/start/{calibreId}", settings);
-
- var request = builder.Build();
+ var request = GetBuilder($"conversion/start/{calibreId}", settings)
+ .AddQueryParam("library_id", settings.Library)
+ .Build();
request.SetContent(options.ToJson());
var jobId = _httpClient.Post(request).Resource;
@@ -223,7 +228,7 @@ public CalibreBook GetBook(int calibreId, CalibreSettings settings)
{
try
{
- var builder = GetBuilder($"ajax/book/{calibreId}", settings);
+ var builder = GetBuilder($"ajax/book/{calibreId}/{settings.Library}", settings);
var request = builder.Build();
var book = _httpClient.Get(request).Resource;
@@ -248,13 +253,13 @@ public List GetAllBookFilePaths(CalibreSettings settings)
var ids = GetAllBookIds(settings);
var result = new List();
- const int count = 100;
var offset = 0;
while (offset < ids.Count)
{
- var builder = GetBuilder($"ajax/books", settings);
- builder.AddQueryParam("ids", ids.Skip(offset).Take(count).ConcatToString(","));
+ var builder = GetBuilder($"ajax/books/{settings.Library}", settings);
+ builder.LogResponseContent = false;
+ builder.AddQueryParam("ids", ids.Skip(offset).Take(PAGE_SIZE).ConcatToString(","));
var request = builder.Build();
try
@@ -279,7 +284,7 @@ public List GetAllBookFilePaths(CalibreSettings settings)
throw new CalibreException("Unable to connect to calibre library: {0}", ex, ex.Message);
}
- offset += count;
+ offset += PAGE_SIZE;
}
return result;
@@ -288,21 +293,20 @@ public List GetAllBookFilePaths(CalibreSettings settings)
public List GetAllBookIds(CalibreSettings settings)
{
// the magic string is 'allbooks' converted to hex
- var builder = GetBuilder($"/ajax/category/616c6c626f6f6b73", settings);
- const int count = 100;
+ var builder = GetBuilder($"/ajax/category/616c6c626f6f6b73/{settings.Library}", settings);
var offset = 0;
var ids = new List();
while (true)
{
- var result = GetPaged(builder, count, offset);
+ var result = GetPaged(builder, PAGE_SIZE, offset);
if (!result.Resource.BookIds.Any())
{
break;
}
- offset += count;
+ offset += PAGE_SIZE;
ids.AddRange(result.Resource.BookIds);
}
@@ -327,18 +331,13 @@ private HttpResponse GetPaged(HttpRequestBuilder builder, int count, int o
}
}
- public void GetLibraryInfo(CalibreSettings settings)
+ private CalibreLibraryInfo GetLibraryInfo(CalibreSettings settings)
{
- try
- {
- var builder = GetBuilder($"ajax/library-info", settings);
- var request = builder.Build();
- var response = _httpClient.Execute(request);
- }
- catch (HttpException ex)
- {
- throw new CalibreException("Unable to connect to calibre library: {0}", ex, ex.Message);
- }
+ var builder = GetBuilder($"ajax/library-info", settings);
+ var request = builder.Build();
+ var response = _httpClient.Get(request);
+
+ return response.Resource;
}
private HttpRequestBuilder GetBuilder(string relativePath, CalibreSettings settings)
@@ -361,8 +360,9 @@ private HttpRequestBuilder GetBuilder(string relativePath, CalibreSettings setti
private async Task PollConvertStatus(long jobId, CalibreSettings settings)
{
- var builder = GetBuilder($"/conversion/status/{jobId}", settings);
- var request = builder.Build();
+ var request = GetBuilder($"/conversion/status/{jobId}", settings)
+ .AddQueryParam("library_id", settings.Library)
+ .Build();
while (true)
{
@@ -381,5 +381,93 @@ private async Task PollConvertStatus(long jobId, CalibreSettings settings)
await Task.Delay(2000);
}
}
+
+ public void Test(CalibreSettings settings)
+ {
+ var failures = new List { TestCalibre(settings) };
+ var validationResult = new ValidationResult(failures);
+ var result = new NzbDroneValidationResult(validationResult.Errors);
+
+ if (!result.IsValid || result.HasWarnings)
+ {
+ throw new ValidationException(result.Failures);
+ }
+ }
+
+ private ValidationFailure TestCalibre(CalibreSettings settings)
+ {
+ var builder = GetBuilder("", settings);
+ builder.Accept(HttpAccept.Html);
+ builder.SuppressHttpError = true;
+
+ var request = builder.Build();
+ request.LogResponseContent = false;
+ HttpResponse response;
+
+ try
+ {
+ response = _httpClient.Execute(request);
+ }
+ catch (WebException ex)
+ {
+ _logger.Error(ex, "Unable to connect to calibre");
+ if (ex.Status == WebExceptionStatus.ConnectFailure)
+ {
+ return new NzbDroneValidationFailure("Host", "Unable to connect")
+ {
+ DetailedDescription = "Please verify the hostname and port."
+ };
+ }
+
+ return new NzbDroneValidationFailure(string.Empty, "Unknown exception: " + ex.Message);
+ }
+
+ if (response.StatusCode == HttpStatusCode.NotFound)
+ {
+ return new ValidationFailure("Host", "Could not connect");
+ }
+
+ if (response.Content.Contains(@"guac-login"))
+ {
+ return new ValidationFailure("Port", "Bad port. This is the container's remote calibre GUI, not the calibre content server. Try mapping port 8081.");
+ }
+
+ if (!response.Content.Contains(@"calibre"))
+ {
+ return new ValidationFailure("Port", "Not a valid calibre content server");
+ }
+
+ CalibreLibraryInfo libraryInfo;
+ try
+ {
+ libraryInfo = GetLibraryInfo(settings);
+ }
+ catch (HttpException e)
+ {
+ if (e.Response.StatusCode == HttpStatusCode.Unauthorized)
+ {
+ return new NzbDroneValidationFailure("Username", "Authentication failure")
+ {
+ DetailedDescription = "Please verify your username and password."
+ };
+ }
+ else
+ {
+ return new NzbDroneValidationFailure(string.Empty, "Unknown exception: " + e.Message);
+ }
+ }
+
+ if (settings.Library.IsNullOrWhiteSpace())
+ {
+ settings.Library = libraryInfo.DefaultLibrary;
+ }
+
+ if (!libraryInfo.LibraryMap.ContainsKey(settings.Library))
+ {
+ return new ValidationFailure("Library", "Not a valid library in calibre");
+ }
+
+ return null;
+ }
}
}
diff --git a/src/NzbDrone.Core/Books/Calibre/CalibreSettings.cs b/src/NzbDrone.Core/Books/Calibre/CalibreSettings.cs
index 25f16900f..53b3ad684 100644
--- a/src/NzbDrone.Core/Books/Calibre/CalibreSettings.cs
+++ b/src/NzbDrone.Core/Books/Calibre/CalibreSettings.cs
@@ -36,6 +36,7 @@ public CalibreSettings()
public string UrlBase { get; set; }
public string Username { get; set; }
public string Password { get; set; }
+ public string Library { get; set; }
public string OutputFormat { get; set; }
public int OutputProfile { get; set; }
public bool UseSsl { get; set; }
diff --git a/src/NzbDrone.Core/Books/Calibre/Resources/CalibreLibraryInfo.cs b/src/NzbDrone.Core/Books/Calibre/Resources/CalibreLibraryInfo.cs
new file mode 100644
index 000000000..2f3f4952b
--- /dev/null
+++ b/src/NzbDrone.Core/Books/Calibre/Resources/CalibreLibraryInfo.cs
@@ -0,0 +1,13 @@
+using System.Collections.Generic;
+using Newtonsoft.Json;
+
+namespace NzbDrone.Core.Books.Calibre
+{
+ public class CalibreLibraryInfo
+ {
+ [JsonProperty("library_map")]
+ public Dictionary LibraryMap { get; set; }
+ [JsonProperty("default_library")]
+ public string DefaultLibrary { get; set; }
+ }
+}
diff --git a/src/NzbDrone.Core/RootFolders/RootFolderService.cs b/src/NzbDrone.Core/RootFolders/RootFolderService.cs
index f3060ebef..368d668b9 100644
--- a/src/NzbDrone.Core/RootFolders/RootFolderService.cs
+++ b/src/NzbDrone.Core/RootFolders/RootFolderService.cs
@@ -2,15 +2,11 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
-using System.Net;
using System.Threading.Tasks;
using NLog;
using NzbDrone.Common;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
-using NzbDrone.Common.Http;
-using NzbDrone.Core.Books.Calibre;
-using NzbDrone.Core.Exceptions;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.Commands;
using NzbDrone.Core.Messaging.Commands;
@@ -34,19 +30,16 @@ public class RootFolderService : IRootFolderService
{
private readonly IRootFolderRepository _rootFolderRepository;
private readonly IDiskProvider _diskProvider;
- private readonly ICalibreProxy _calibreProxy;
private readonly IManageCommandQueue _commandQueueManager;
private readonly Logger _logger;
public RootFolderService(IRootFolderRepository rootFolderRepository,
- ICalibreProxy calibreProxy,
IDiskProvider diskProvider,
IManageCommandQueue commandQueueManager,
Logger logger)
{
_rootFolderRepository = rootFolderRepository;
_diskProvider = diskProvider;
- _calibreProxy = calibreProxy;
_commandQueueManager = commandQueueManager;
_logger = logger;
}
@@ -98,12 +91,6 @@ private void VerifyRootFolder(RootFolder rootFolder)
{
throw new UnauthorizedAccessException(string.Format("Root folder path '{0}' is not writable by user '{1}'", rootFolder.Path, Environment.UserName));
}
-
- if (rootFolder.IsCalibreLibrary)
- {
- // This will throw on failure
- _calibreProxy.GetLibraryInfo(rootFolder.CalibreSettings);
- }
}
public RootFolder Add(RootFolder rootFolder)
diff --git a/src/Readarr.Api.V1/RootFolders/RootFolderModule.cs b/src/Readarr.Api.V1/RootFolders/RootFolderModule.cs
index 808d16eba..706da7ff8 100644
--- a/src/Readarr.Api.V1/RootFolders/RootFolderModule.cs
+++ b/src/Readarr.Api.V1/RootFolders/RootFolderModule.cs
@@ -16,8 +16,10 @@ namespace Readarr.Api.V1.RootFolders
public class RootFolderModule : ReadarrRestModuleWithSignalR
{
private readonly IRootFolderService _rootFolderService;
+ private readonly ICalibreProxy _calibreProxy;
public RootFolderModule(IRootFolderService rootFolderService,
+ ICalibreProxy calibreProxy,
IBroadcastSignalRMessage signalRBroadcaster,
RootFolderValidator rootFolderValidator,
PathExistsValidator pathExistsValidator,
@@ -30,6 +32,7 @@ public RootFolderModule(IRootFolderService rootFolderService,
: base(signalRBroadcaster)
{
_rootFolderService = rootFolderService;
+ _calibreProxy = calibreProxy;
GetResourceAll = GetRootFolders;
GetResourceById = GetRootFolder;
@@ -76,6 +79,11 @@ private int CreateRootFolder(RootFolderResource rootFolderResource)
{
var model = rootFolderResource.ToModel();
+ if (model.IsCalibreLibrary)
+ {
+ _calibreProxy.Test(model.CalibreSettings);
+ }
+
return _rootFolderService.Add(model).Id;
}
diff --git a/src/Readarr.Api.V1/RootFolders/RootFolderResource.cs b/src/Readarr.Api.V1/RootFolders/RootFolderResource.cs
index 946ebbc7a..aa723206b 100644
--- a/src/Readarr.Api.V1/RootFolders/RootFolderResource.cs
+++ b/src/Readarr.Api.V1/RootFolders/RootFolderResource.cs
@@ -21,6 +21,7 @@ public class RootFolderResource : RestResource
public string UrlBase { get; set; }
public string Username { get; set; }
public string Password { get; set; }
+ public string Library { get; set; }
public string OutputFormat { get; set; }
public int OutputProfile { get; set; }
public bool UseSsl { get; set; }
@@ -55,6 +56,7 @@ public static RootFolderResource ToResource(this RootFolder model)
UrlBase = model.CalibreSettings?.UrlBase,
Username = model.CalibreSettings?.Username,
Password = model.CalibreSettings?.Password,
+ Library = model.CalibreSettings?.Library,
OutputFormat = model.CalibreSettings?.OutputFormat,
OutputProfile = model.CalibreSettings?.OutputProfile ?? 0,
UseSsl = model.CalibreSettings?.UseSsl ?? false,
@@ -82,6 +84,7 @@ public static RootFolder ToModel(this RootFolderResource resource)
UrlBase = resource.UrlBase,
Username = resource.Username,
Password = resource.Password,
+ Library = resource.Library,
OutputFormat = resource.OutputFormat,
OutputProfile = resource.OutputProfile,
UseSsl = resource.UseSsl