New: Calibre library support

pull/755/head
ta264 3 years ago
parent a579a93aab
commit 16e04041a9

@ -49,6 +49,7 @@ function EditRootFolderModalContent(props) {
urlBase,
username,
password,
library,
outputFormat,
outputProfile,
useSsl
@ -100,7 +101,7 @@ function EditRootFolderModalContent(props) {
</FormGroup>
<FormGroup>
<FormLabel>Calibre Library</FormLabel>
<FormLabel>Use Calibre</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
@ -177,6 +178,18 @@ function EditRootFolderModalContent(props) {
/>
</FormGroup>
<FormGroup>
<FormLabel>Calibre Library</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="library"
helpText="Calibre content server library name. Leave blank for default."
{...library}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>Convert to format</FormLabel>

@ -4,6 +4,8 @@ using System.IO;
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.Common.Serializer;
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<string> formats, CalibreSettings settings);
@ -27,10 +29,13 @@ namespace NzbDrone.Core.Books.Calibre
long ConvertBook(int calibreId, CalibreConversionOptions options, CalibreSettings settings);
List<string> 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 @@ namespace NzbDrone.Core.Books.Calibre
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 @@ namespace NzbDrone.Core.Books.Calibre
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 @@ namespace NzbDrone.Core.Books.Calibre
{
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<CalibreBookData>(request).Resource;
}
@ -201,9 +206,9 @@ namespace NzbDrone.Core.Books.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<long>(request).Resource;
@ -223,7 +228,7 @@ namespace NzbDrone.Core.Books.Calibre
{
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<CalibreBook>(request).Resource;
@ -248,13 +253,13 @@ namespace NzbDrone.Core.Books.Calibre
var ids = GetAllBookIds(settings);
var result = new List<string>();
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 @@ namespace NzbDrone.Core.Books.Calibre
throw new CalibreException("Unable to connect to calibre library: {0}", ex, ex.Message);
}
offset += count;
offset += PAGE_SIZE;
}
return result;
@ -288,21 +293,20 @@ namespace NzbDrone.Core.Books.Calibre
public List<int> 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<int>();
while (true)
{
var result = GetPaged<CalibreCategory>(builder, count, offset);
var result = GetPaged<CalibreCategory>(builder, PAGE_SIZE, offset);
if (!result.Resource.BookIds.Any())
{
break;
}
offset += count;
offset += PAGE_SIZE;
ids.AddRange(result.Resource.BookIds);
}
@ -327,18 +331,13 @@ namespace NzbDrone.Core.Books.Calibre
}
}
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<CalibreLibraryInfo>(request);
return response.Resource;
}
private HttpRequestBuilder GetBuilder(string relativePath, CalibreSettings settings)
@ -361,8 +360,9 @@ namespace NzbDrone.Core.Books.Calibre
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 @@ namespace NzbDrone.Core.Books.Calibre
await Task.Delay(2000);
}
}
public void Test(CalibreSettings settings)
{
var failures = new List<ValidationFailure> { 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(@"<title>calibre</title>"))
{
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;
}
}
}

@ -36,6 +36,7 @@ namespace NzbDrone.Core.Books.Calibre
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; }

@ -0,0 +1,13 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace NzbDrone.Core.Books.Calibre
{
public class CalibreLibraryInfo
{
[JsonProperty("library_map")]
public Dictionary<string, string> LibraryMap { get; set; }
[JsonProperty("default_library")]
public string DefaultLibrary { get; set; }
}
}

@ -2,15 +2,11 @@ using System;
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 @@ namespace NzbDrone.Core.RootFolders
{
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 @@ namespace NzbDrone.Core.RootFolders
{
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)

@ -16,8 +16,10 @@ namespace Readarr.Api.V1.RootFolders
public class RootFolderModule : ReadarrRestModuleWithSignalR<RootFolderResource, RootFolder>
{
private readonly IRootFolderService _rootFolderService;
private readonly ICalibreProxy _calibreProxy;
public RootFolderModule(IRootFolderService rootFolderService,
ICalibreProxy calibreProxy,
IBroadcastSignalRMessage signalRBroadcaster,
RootFolderValidator rootFolderValidator,
PathExistsValidator pathExistsValidator,
@ -30,6 +32,7 @@ namespace Readarr.Api.V1.RootFolders
: base(signalRBroadcaster)
{
_rootFolderService = rootFolderService;
_calibreProxy = calibreProxy;
GetResourceAll = GetRootFolders;
GetResourceById = GetRootFolder;
@ -76,6 +79,11 @@ namespace Readarr.Api.V1.RootFolders
{
var model = rootFolderResource.ToModel();
if (model.IsCalibreLibrary)
{
_calibreProxy.Test(model.CalibreSettings);
}
return _rootFolderService.Add(model).Id;
}

@ -21,6 +21,7 @@ namespace Readarr.Api.V1.RootFolders
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 @@ namespace Readarr.Api.V1.RootFolders
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 @@ namespace Readarr.Api.V1.RootFolders
UrlBase = resource.UrlBase,
Username = resource.Username,
Password = resource.Password,
Library = resource.Library,
OutputFormat = resource.OutputFormat,
OutputProfile = resource.OutputProfile,
UseSsl = resource.UseSsl

Loading…
Cancel
Save