mirror of
https://github.com/Readarr/Readarr
synced 2025-12-16 05:12:42 +01:00
New: Calibre library support
This commit is contained in:
parent
a579a93aab
commit
16e04041a9
7 changed files with 158 additions and 45 deletions
|
|
@ -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.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<string> formats, CalibreSettings settings);
|
||||
|
|
@ -27,10 +29,13 @@ public interface ICalibreProxy
|
|||
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 @@ 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<CalibreBookData>(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<long>(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<CalibreBook>(request).Resource;
|
||||
|
|
@ -248,13 +253,13 @@ public List<string> GetAllBookFilePaths(CalibreSettings settings)
|
|||
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 @@ public List<string> 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<string> GetAllBookFilePaths(CalibreSettings settings)
|
|||
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 @@ private HttpResponse<T> GetPaged<T>(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<CalibreLibraryInfo>(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<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 @@ 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; }
|
||||
|
|
|
|||
|
|
@ -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.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)
|
||||
|
|
|
|||
|
|
@ -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 @@ 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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue