New: Kavita Connection (#1880)

* Added ability for Readarr to inform Kavita when a change occurs for rescan.

* Use the existing API with a POST rather than a new API.

* Updated some wording

* Fixed PR comments
pull/1884/head
Joe Milazzo 2 years ago committed by GitHub
parent c3cbbb7627
commit 14d74f2eca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,74 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Extensions;
namespace NzbDrone.Core.Notifications.Kavita;
public class Kavita : NotificationBase<KavitaSettings>
{
private readonly IKavitaService _kavitaService;
private readonly Logger _logger;
public Kavita(IKavitaService kavitaService, Logger logger)
{
_kavitaService = kavitaService;
_logger = logger;
}
public override string Link => "https://www.kavitareader.com/";
public override void OnReleaseImport(BookDownloadMessage message)
{
var allPaths = message.BookFiles.Select(v => v.Path).Distinct();
var path = Directory.GetParent(allPaths.First())?.FullName;
Notify(Settings, BOOK_DOWNLOADED_TITLE_BRANDED, path);
}
public override void OnBookDelete(BookDeleteMessage deleteMessage)
{
var allPaths = deleteMessage.Book.BookFiles.Value.Select(v => v.Path).Distinct();
var path = Directory.GetParent(allPaths.First())?.FullName;
Notify(Settings, BOOK_FILE_DELETED_TITLE_BRANDED, path);
}
public override void OnBookFileDelete(BookFileDeleteMessage message)
{
Notify(Settings, BOOK_FILE_DELETED_TITLE_BRANDED, Directory.GetParent(message.BookFile.Path)?.FullName);
}
public override void OnBookRetag(BookRetagMessage message)
{
Notify(Settings, BOOK_RETAGGED_TITLE_BRANDED, Directory.GetParent(message.BookFile.Path)?.FullName);
}
public override string Name => "Kavita";
public override ValidationResult Test()
{
var failures = new List<ValidationFailure>();
failures.AddIfNotNull(_kavitaService.Test(Settings, "Success! Kavita has been successfully configured!"));
return new ValidationResult(failures);
}
private void Notify(KavitaSettings settings, string header, string message)
{
try
{
if (Settings.Notify)
{
_kavitaService.Notify(Settings, $"{header} - {message}");
}
}
catch (SocketException ex)
{
var logMessage = $"Unable to connect to Subsonic Host: {Settings.Host}:{Settings.Port}";
_logger.Debug(ex, logMessage);
}
}
}

@ -0,0 +1,14 @@
namespace NzbDrone.Core.Notifications.Kavita;
public class KavitaAuthenticationException : KavitaException
{
public KavitaAuthenticationException(string message)
: base(message)
{
}
public KavitaAuthenticationException(string message, params object[] args)
: base(message, args)
{
}
}

@ -0,0 +1,11 @@
using System.Text.Json.Serialization;
namespace NzbDrone.Core.Notifications.Kavita;
public class KavitaAuthenticationResult
{
[JsonPropertyName("token")]
public string Token { get; set; }
[JsonPropertyName("apiKey")]
public string ApiKey { get; init; }
}

@ -0,0 +1,16 @@
using NzbDrone.Common.Exceptions;
namespace NzbDrone.Core.Notifications.Kavita;
public class KavitaException : NzbDroneException
{
public KavitaException(string message)
: base(message)
{
}
public KavitaException(string message, params object[] args)
: base(message, args)
{
}
}

@ -0,0 +1,56 @@
using System;
using FluentValidation.Results;
using NLog;
namespace NzbDrone.Core.Notifications.Kavita;
public interface IKavitaService
{
void Notify(KavitaSettings settings, string message);
ValidationFailure Test(KavitaSettings settings, string message);
}
public class KavitaService : IKavitaService
{
private readonly IKavitaServiceProxy _proxy;
private readonly Logger _logger;
public KavitaService(IKavitaServiceProxy proxy,
Logger logger)
{
_proxy = proxy;
_logger = logger;
}
public void Notify(KavitaSettings settings, string folderPath)
{
_proxy.Notify(settings, folderPath);
}
private string GetToken(KavitaSettings settings)
{
return _proxy.GetToken(settings);
}
public ValidationFailure Test(KavitaSettings settings, string message)
{
try
{
_logger.Debug("Determining Authentication of Host: {0}", _proxy.GetBaseUrl(settings));
var token = GetToken(settings);
_logger.Debug("Token is: {0}", token);
}
catch (KavitaAuthenticationException ex)
{
_logger.Error(ex, "Unable to connect to Kavita Server");
return new ValidationFailure("ApiKey", "Incorrect ApiKey");
}
catch (Exception ex)
{
_logger.Error(ex, "Unable to connect to Kavita Server");
return new ValidationFailure("Host", "Unable to connect to Kavita Server");
}
return null;
}
}

@ -0,0 +1,87 @@
using System.Net.Http;
using System.Text.Json;
using NLog;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer;
namespace NzbDrone.Core.Notifications.Kavita;
public interface IKavitaServiceProxy
{
string GetBaseUrl(KavitaSettings settings, string relativePath = null);
void Notify(KavitaSettings settings, string message);
string GetToken(KavitaSettings settings);
}
public class KavitaServiceProxy : IKavitaServiceProxy
{
private readonly IHttpClient _httpClient;
private readonly Logger _logger;
public KavitaServiceProxy(IHttpClient httpClient, Logger logger)
{
_httpClient = httpClient;
_logger = logger;
}
public string GetBaseUrl(KavitaSettings settings, string relativePath = null)
{
var baseUrl = HttpRequestBuilder.BuildBaseUrl(settings.UseSsl, settings.Host, settings.Port, string.Empty);
baseUrl = HttpUri.CombinePath(baseUrl, relativePath);
return baseUrl;
}
public void Notify(KavitaSettings settings, string folderPath)
{
var request = GetKavitaServerRequest("library/scan-folder", HttpMethod.Post, settings);
request.Headers.ContentType = "application/json";
var postRequest = request.Build();
postRequest.SetContent(new
{
ApiKey = settings.ApiKey,
FolderPath = folderPath.Replace("/", "//")
}.ToJson());
var response = _httpClient.Post(postRequest);
_logger.Trace("Update response: {0}", string.IsNullOrEmpty(response.Content) ? "Success" : response.Content);
}
public string GetToken(KavitaSettings settings)
{
var request = GetKavitaServerRequest("plugin/authenticate", HttpMethod.Post, settings);
request.AddQueryParam("apiKey", settings.ApiKey)
.AddQueryParam("pluginName", BuildInfo.AppName);
var response = _httpClient.Execute(request.Build());
_logger.Trace("Authenticate response: {0}", response.Content);
var authResult = JsonSerializer.Deserialize<KavitaAuthenticationResult>(response.Content);
if (authResult == null)
{
throw new KavitaException("Could not authenticate with Kavita");
}
return authResult.Token;
}
private HttpRequestBuilder GetKavitaServerRequest(string resource, HttpMethod method, KavitaSettings settings)
{
var client = new HttpRequestBuilder(GetBaseUrl(settings, "api"));
client.Resource(resource);
if (settings.ApiKey.IsNotNullOrWhiteSpace())
{
client.Headers["x-kavita-apikey"] = settings.ApiKey;
client.Headers["x-kavita-plugin"] = BuildInfo.AppName;
}
client.Method = method;
return client;
}
}

@ -0,0 +1,46 @@
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Notifications.Kavita;
public class KavitaSettingsValidator : AbstractValidator<KavitaSettings>
{
public KavitaSettingsValidator()
{
RuleFor(c => c.Host).ValidHost();
RuleFor(c => c.Port).InclusiveBetween(1, 65535);
RuleFor(c => c.ApiKey).NotEmpty();
}
}
public class KavitaSettings : IProviderConfig
{
private static readonly KavitaSettingsValidator Validator = new KavitaSettingsValidator();
public KavitaSettings()
{
Port = 4040;
}
[FieldDefinition(0, Label = "Host")]
public string Host { get; set; }
[FieldDefinition(1, Label = "Port")]
public int Port { get; set; }
[FieldDefinition(2, Label = "API Key", Privacy = PrivacyLevel.ApiKey, HelpLink = "https://wiki.kavitareader.com/en/guides/settings/opds")]
public string ApiKey { get; set; }
[FieldDefinition(3, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Connect to Kavita over HTTPS instead of HTTP")]
public bool UseSsl { get; set; }
[FieldDefinition(4, Label = "Update Library", Type = FieldType.Checkbox)]
public bool Notify { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
Loading…
Cancel
Save