mirror of
https://github.com/Readarr/Readarr
synced 2025-12-14 20:36:18 +01:00
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
This commit is contained in:
parent
c3cbbb7627
commit
14d74f2eca
7 changed files with 304 additions and 0 deletions
74
src/NzbDrone.Core/Notifications/Kavita/Kavita.cs
Normal file
74
src/NzbDrone.Core/Notifications/Kavita/Kavita.cs
Normal file
|
|
@ -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; }
|
||||
}
|
||||
16
src/NzbDrone.Core/Notifications/Kavita/KavitaException.cs
Normal file
16
src/NzbDrone.Core/Notifications/Kavita/KavitaException.cs
Normal file
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
56
src/NzbDrone.Core/Notifications/Kavita/KavitaService.cs
Normal file
56
src/NzbDrone.Core/Notifications/Kavita/KavitaService.cs
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
87
src/NzbDrone.Core/Notifications/Kavita/KavitaServiceProxy.cs
Normal file
87
src/NzbDrone.Core/Notifications/Kavita/KavitaServiceProxy.cs
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
46
src/NzbDrone.Core/Notifications/Kavita/KavitaSettings.cs
Normal file
46
src/NzbDrone.Core/Notifications/Kavita/KavitaSettings.cs
Normal file
|
|
@ -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…
Reference in a new issue