mirror of
https://github.com/Prowlarr/Prowlarr
synced 2026-05-09 05:22:09 +02:00
Add Listenarr application integration
Introduces Listenarr support with backend models, settings, proxy, and core logic for indexer synchronization. Adds frontend component for Listenarr settings modal and comprehensive unit tests for Listenarr integration and proxy behavior.
This commit is contained in:
parent
688434ced9
commit
e08505f931
7 changed files with 636 additions and 0 deletions
20
frontend/src/Settings/Applications/Listenarr/Listenarr.tsx
Normal file
20
frontend/src/Settings/Applications/Listenarr/Listenarr.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import React from 'react';
|
||||
import { Application } from 'Settings/Applications/Application';
|
||||
import ProviderSettingsModal from 'Settings/Applications/ProviderSettingsModal';
|
||||
|
||||
interface ListenarrProps {
|
||||
selectedApplication: Application;
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
function Listenarr({ selectedApplication, onModalClose }: ListenarrProps) {
|
||||
return (
|
||||
<ProviderSettingsModal
|
||||
providerData={selectedApplication}
|
||||
section="applications"
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default Listenarr;
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Applications;
|
||||
using NzbDrone.Core.Applications.Listenarr;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Profiles;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Test.Common;
|
||||
|
||||
namespace NzbDrone.Core.Test.Applications.Listenarr
|
||||
{
|
||||
[TestFixture]
|
||||
public class ListenarrFixture : CoreTest<NzbDrone.Core.Applications.Listenarr.Listenarr>
|
||||
{
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
Subject.Definition = new ApplicationDefinition
|
||||
{
|
||||
Settings = new ListenarrSettings
|
||||
{
|
||||
ProwlarrUrl = "http://localhost:9696",
|
||||
BaseUrl = "http://localhost:5000",
|
||||
ApiKey = "abc",
|
||||
SyncCategories = new List<int> { NewznabStandardCategory.Movies.Id }
|
||||
}
|
||||
};
|
||||
|
||||
Mocker.GetMock<IConfigFileProvider>().SetupGet(c => c.ApiKey).Returns("abc");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetIndexerMappings_should_return_mappings_when_baseUrl_matches_prowlarr()
|
||||
{
|
||||
// Arrange
|
||||
var indexer = new ListenarrIndexer
|
||||
{
|
||||
Id = 99,
|
||||
Implementation = "Newznab",
|
||||
Fields = new List<ListenarrField>
|
||||
{
|
||||
new ListenarrField { Name = "baseUrl", Value = "http://localhost:9696/45/api" },
|
||||
new ListenarrField { Name = "apiKey", Value = "abc" }
|
||||
}
|
||||
};
|
||||
|
||||
Mocker.GetMock<IListenarrV1Proxy>().Setup(c => c.GetIndexers(It.IsAny<ListenarrSettings>())).Returns(new List<ListenarrIndexer> { indexer });
|
||||
|
||||
// Act
|
||||
var mappings = Subject.GetIndexerMappings();
|
||||
|
||||
// Assert
|
||||
mappings.Should().HaveCount(1);
|
||||
mappings[0].IndexerId.Should().Be(45);
|
||||
mappings[0].RemoteIndexerId.Should().Be(99);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetIndexerMappings_should_skip_non_matching_api_key_and_baseurl()
|
||||
{
|
||||
// Arrange
|
||||
var indexer = new ListenarrIndexer
|
||||
{
|
||||
Id = 100,
|
||||
Implementation = "Newznab",
|
||||
Fields = new List<ListenarrField>
|
||||
{
|
||||
new ListenarrField { Name = "baseUrl", Value = "http://external/1/api" },
|
||||
new ListenarrField { Name = "apiKey", Value = "wrong" }
|
||||
}
|
||||
};
|
||||
|
||||
Mocker.GetMock<IListenarrV1Proxy>().Setup(c => c.GetIndexers(It.IsAny<ListenarrSettings>())).Returns(new List<ListenarrIndexer> { indexer });
|
||||
|
||||
// Act
|
||||
var mappings = Subject.GetIndexerMappings();
|
||||
|
||||
// Assert
|
||||
mappings.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_should_fail_when_status_null()
|
||||
{
|
||||
// Arrange
|
||||
Mocker.GetMock<IListenarrV1Proxy>().Setup(c => c.GetStatus(It.IsAny<ListenarrSettings>())).Returns((ListenarrStatus)null);
|
||||
|
||||
// Act
|
||||
var result = Subject.Test();
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().ContainSingle(e => e.ErrorMessage.Contains("Unable to connect to Listenarr"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_should_fail_on_exception()
|
||||
{
|
||||
// Arrange
|
||||
Mocker.GetMock<IListenarrV1Proxy>().Setup(c => c.GetStatus(It.IsAny<ListenarrSettings>())).Throws(new Exception("boom"));
|
||||
|
||||
// Act
|
||||
var result = Subject.Test();
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().ContainSingle(e => e.ErrorMessage.Contains("Unable to send test message"));
|
||||
|
||||
// expected error was logged
|
||||
ExceptionVerification.ExpectedErrors(1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddIndexer_should_insert_app_indexer_mapping_on_success()
|
||||
{
|
||||
// Arrange
|
||||
var indexerDefinition = new IndexerDefinition
|
||||
{
|
||||
Id = 12,
|
||||
Name = "TestIndexer",
|
||||
Protocol = DownloadProtocol.Usenet,
|
||||
Capabilities = new IndexerCapabilities(),
|
||||
Enable = true,
|
||||
AppProfile = new LazyLoaded<AppSyncProfile>(new AppSyncProfile { EnableRss = true, EnableAutomaticSearch = true, EnableInteractiveSearch = true })
|
||||
};
|
||||
|
||||
// Add a category that matches Settings.SyncCategories
|
||||
indexerDefinition.Capabilities.Categories.AddCategoryMapping(1, NewznabStandardCategory.Movies);
|
||||
|
||||
var mockIndexer = new Mock<IIndexer>();
|
||||
mockIndexer.Setup(i => i.GetCapabilities()).Returns(indexerDefinition.Capabilities);
|
||||
|
||||
Mocker.GetMock<IIndexerFactory>().Setup(m => m.GetInstance(It.IsAny<IndexerDefinition>())).Returns(mockIndexer.Object);
|
||||
|
||||
Mocker.GetMock<IListenarrV1Proxy>().Setup(c => c.AddIndexer(It.IsAny<ListenarrIndexer>(), It.IsAny<ListenarrSettings>())).Returns(new ListenarrIndexer { Id = 501 });
|
||||
|
||||
// pre-check
|
||||
indexerDefinition.Capabilities.Categories.SupportedCategories(((ListenarrSettings)Subject.Definition.Settings).SyncCategories.ToArray()).Should().NotBeEmpty();
|
||||
|
||||
// Act
|
||||
Subject.AddIndexer(indexerDefinition);
|
||||
|
||||
// Assert
|
||||
Mocker.GetMock<IListenarrV1Proxy>().Verify(m => m.AddIndexer(It.IsAny<ListenarrIndexer>(), It.IsAny<ListenarrSettings>()), Times.Once());
|
||||
Mocker.GetMock<IAppIndexerMapService>().Verify(m => m.Insert(It.Is<AppIndexerMap>(a => a.IndexerId == 12 && a.RemoteIndexerId == 501)), Times.Once());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
using System;
|
||||
using System.Net;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Applications.Listenarr;
|
||||
using NzbDrone.Test.Common;
|
||||
|
||||
namespace NzbDrone.Core.Test.Applications.Listenarr
|
||||
{
|
||||
[TestFixture]
|
||||
public class ListenarrV1ProxyFixture : TestBase<ListenarrV1Proxy>
|
||||
{
|
||||
[Test]
|
||||
public void GetIndexers_should_deserialize_json_and_set_api_key_header()
|
||||
{
|
||||
// Arrange
|
||||
var settings = new ListenarrSettings { BaseUrl = "http://localhost:5000", ApiKey = "abc123" };
|
||||
|
||||
var json = "[ { \"id\": 42, \"name\": \"Test\", \"implementation\": \"Newznab\", \"fields\": [ { \"name\": \"baseUrl\", \"value\": \"http://localhost:5000/1/api\" }, { \"name\": \"apiKey\", \"value\": \"x\" } ] } ]";
|
||||
|
||||
HttpRequest capturedRequest = null;
|
||||
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(c => c.Execute(It.IsAny<HttpRequest>()))
|
||||
.Returns<HttpRequest>(req =>
|
||||
{
|
||||
capturedRequest = req;
|
||||
return new HttpResponse(req, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), json, 0, HttpStatusCode.OK, new Version("1.0"));
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = Subject.GetIndexers(settings);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.Count.Should().Be(1);
|
||||
capturedRequest.Headers.GetSingleValue("X-Api-Key").Should().Be("abc123");
|
||||
capturedRequest.Url.ToString().Should().Contain("/api/indexer");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Execute_should_throw_application_exception_when_unauthorized()
|
||||
{
|
||||
// Arrange
|
||||
var settings = new ListenarrSettings { BaseUrl = "http://localhost:5000", ApiKey = "bad" };
|
||||
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(c => c.Execute(It.IsAny<HttpRequest>()))
|
||||
.Throws(new HttpException(new HttpResponse(new HttpRequest("http://localhost/"), new HttpHeader(), new CookieCollection(), "unauthorized", 0, HttpStatusCode.Unauthorized, new Version("1.0"))));
|
||||
|
||||
// Act / Assert
|
||||
Assert.Throws<ApplicationException>(() => Subject.GetIndexers(settings));
|
||||
|
||||
// expected error was logged
|
||||
ExceptionVerification.ExpectedErrors(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
197
src/NzbDrone.Core/Applications/Listenarr/Listenarr.cs
Normal file
197
src/NzbDrone.Core/Applications/Listenarr/Listenarr.cs
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FluentValidation.Results;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Tags;
|
||||
|
||||
namespace NzbDrone.Core.Applications.Listenarr
|
||||
{
|
||||
public class Listenarr : ApplicationBase<ListenarrSettings>
|
||||
{
|
||||
public override string Name => "Listenarr";
|
||||
|
||||
private readonly IListenarrV1Proxy _listenarrV1Proxy;
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
private readonly Lazy<ITagService> _tagService;
|
||||
|
||||
public Listenarr(
|
||||
IListenarrV1Proxy listenarrV1Proxy,
|
||||
IAppIndexerMapService appIndexerMapService,
|
||||
IIndexerFactory indexerFactory,
|
||||
IConfigFileProvider configFileProvider,
|
||||
Lazy<ITagService> tagService,
|
||||
Logger logger)
|
||||
: base(appIndexerMapService, indexerFactory, logger)
|
||||
{
|
||||
_listenarrV1Proxy = listenarrV1Proxy;
|
||||
_configFileProvider = configFileProvider;
|
||||
_tagService = tagService;
|
||||
}
|
||||
|
||||
public override ValidationResult Test()
|
||||
{
|
||||
var failures = new List<ValidationFailure>();
|
||||
|
||||
try
|
||||
{
|
||||
var status = _listenarrV1Proxy.GetStatus(Settings);
|
||||
|
||||
if (status == null)
|
||||
{
|
||||
failures.Add(new ValidationFailure(string.Empty, "Unable to connect to Listenarr"));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Unable to send test message");
|
||||
failures.Add(new ValidationFailure(string.Empty, "Unable to send test message"));
|
||||
}
|
||||
|
||||
return new ValidationResult(failures);
|
||||
}
|
||||
|
||||
public override List<AppIndexerMap> GetIndexerMappings()
|
||||
{
|
||||
var indexers = _listenarrV1Proxy.GetIndexers(Settings)?.Where(i => i.Implementation is "Newznab" or "Torznab");
|
||||
var mappings = new List<AppIndexerMap>();
|
||||
|
||||
foreach (var indexer in indexers ?? Enumerable.Empty<ListenarrIndexer>())
|
||||
{
|
||||
var baseUrl = indexer.Fields?.FirstOrDefault(x => x.Name == "baseUrl")?.Value?.ToString() ?? indexer.BaseUrl ?? string.Empty;
|
||||
|
||||
if (!baseUrl.StartsWith(Settings.ProwlarrUrl.TrimEnd('/')) &&
|
||||
(string)indexer.Fields?.FirstOrDefault(x => x.Name == "apiKey")?.Value != _configFileProvider.ApiKey)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var match = AppIndexerRegex.Match(baseUrl);
|
||||
|
||||
if (match.Groups["indexer"].Success && int.TryParse(match.Groups["indexer"].Value, out var indexerId))
|
||||
{
|
||||
mappings.Add(new AppIndexerMap
|
||||
{
|
||||
IndexerId = indexerId,
|
||||
RemoteIndexerId = indexer.Id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return mappings;
|
||||
}
|
||||
|
||||
public override void AddIndexer(IndexerDefinition indexer)
|
||||
{
|
||||
var indexerCapabilities = GetIndexerCapabilities(indexer);
|
||||
|
||||
if (!indexerCapabilities.SearchAvailable)
|
||||
{
|
||||
_logger.Debug("Skipping add for indexer {0} [{1}] due to missing search support by the indexer", indexer.Name, indexer.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Empty())
|
||||
{
|
||||
_logger.Debug("Skipping add for indexer {0} [{1}] due to no app Sync Categories supported by the indexer", indexer.Name, indexer.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.Trace("Adding indexer {0} [{1}]", indexer.Name, indexer.Id);
|
||||
|
||||
var listenarrIndexer = BuildListenarrIndexer(indexer, Settings);
|
||||
|
||||
var remoteIndexer = _listenarrV1Proxy.AddIndexer(listenarrIndexer, Settings);
|
||||
|
||||
if (remoteIndexer == null)
|
||||
{
|
||||
_logger.Debug("Failed to add {0} [{1}]", indexer.Name, indexer.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
_appIndexerMapService.Insert(new AppIndexerMap
|
||||
{
|
||||
AppId = Definition.Id,
|
||||
IndexerId = indexer.Id,
|
||||
RemoteIndexerId = remoteIndexer.Id
|
||||
});
|
||||
}
|
||||
|
||||
public override void RemoveIndexer(int indexerId)
|
||||
{
|
||||
var appMappings = _appIndexerMapService.GetMappingsForApp(Definition.Id);
|
||||
var mapping = appMappings.FirstOrDefault(m => m.IndexerId == indexerId);
|
||||
|
||||
if (mapping != null)
|
||||
{
|
||||
_listenarrV1Proxy.RemoveIndexer(mapping.RemoteIndexerId, Settings);
|
||||
_appIndexerMapService.Delete(mapping.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public override void UpdateIndexer(IndexerDefinition indexer, bool forceSync = false)
|
||||
{
|
||||
_logger.Debug("Updating indexer {0} [{1}]", indexer.Name, indexer.Id);
|
||||
|
||||
var indexerCapabilities = GetIndexerCapabilities(indexer);
|
||||
var appMappings = _appIndexerMapService.GetMappingsForApp(Definition.Id);
|
||||
var mapping = appMappings.FirstOrDefault(m => m.IndexerId == indexer.Id);
|
||||
|
||||
if (mapping != null)
|
||||
{
|
||||
var listenarrIndexer = BuildListenarrIndexer(indexer, Settings, mapping.RemoteIndexerId);
|
||||
_listenarrV1Proxy.UpdateIndexer(listenarrIndexer, Settings);
|
||||
}
|
||||
}
|
||||
|
||||
public override object RequestAction(string action, IDictionary<string, string> query)
|
||||
{
|
||||
if (action == "getTags")
|
||||
{
|
||||
var tags = _tagService.Value.All().Select(t => new { Value = t.Id, Name = t.Label });
|
||||
return new { options = tags };
|
||||
}
|
||||
|
||||
return base.RequestAction(action, query);
|
||||
}
|
||||
|
||||
private ListenarrIndexer BuildListenarrIndexer(IndexerDefinition indexer, ListenarrSettings settings, int remoteId = 0)
|
||||
{
|
||||
var listenarrIndexer = new ListenarrIndexer
|
||||
{
|
||||
Id = remoteId,
|
||||
Name = $"{indexer.Name} (Prowlarr)",
|
||||
EnableRss = indexer.Enable && indexer.AppProfile.Value.EnableRss,
|
||||
EnableAutomaticSearch = indexer.Enable && indexer.AppProfile.Value.EnableAutomaticSearch,
|
||||
EnableInteractiveSearch = indexer.Enable && indexer.AppProfile.Value.EnableInteractiveSearch,
|
||||
Priority = indexer.Priority,
|
||||
ConfigContract = "NewznabSettings",
|
||||
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
|
||||
Protocol = indexer.Protocol == DownloadProtocol.Usenet ? "usenet" : "torrent",
|
||||
Fields = new List<ListenarrField>
|
||||
{
|
||||
new ListenarrField
|
||||
{
|
||||
Name = "baseUrl",
|
||||
Value = $"{settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/api"
|
||||
},
|
||||
new ListenarrField
|
||||
{
|
||||
Name = "apiKey",
|
||||
Value = _configFileProvider.ApiKey
|
||||
},
|
||||
new ListenarrField
|
||||
{
|
||||
Name = "categories",
|
||||
Value = Array.Empty<int>()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return listenarrIndexer;
|
||||
}
|
||||
}
|
||||
}
|
||||
37
src/NzbDrone.Core/Applications/Listenarr/ListenarrModels.cs
Normal file
37
src/NzbDrone.Core/Applications/Listenarr/ListenarrModels.cs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace NzbDrone.Core.Applications.Listenarr
|
||||
{
|
||||
public class ListenarrStatus
|
||||
{
|
||||
public string Version { get; set; }
|
||||
}
|
||||
|
||||
public class ListenarrIndexer
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public bool EnableRss { get; set; }
|
||||
public bool EnableAutomaticSearch { get; set; }
|
||||
public bool EnableInteractiveSearch { get; set; }
|
||||
public int Priority { get; set; }
|
||||
public string ConfigContract { get; set; }
|
||||
public string Implementation { get; set; }
|
||||
public string Protocol { get; set; }
|
||||
public string BaseUrl { get; set; }
|
||||
public List<ListenarrField> Fields { get; set; }
|
||||
public List<int> Tags { get; set; }
|
||||
}
|
||||
|
||||
public class ListenarrField
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public object Value { get; set; }
|
||||
}
|
||||
|
||||
public class ListenarrTag
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Label { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
using System.Collections.Generic;
|
||||
using FluentValidation;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Applications.Listenarr
|
||||
{
|
||||
public class ListenarrSettingsValidator : AbstractValidator<ListenarrSettings>
|
||||
{
|
||||
public ListenarrSettingsValidator()
|
||||
{
|
||||
RuleFor(c => c.BaseUrl).IsValidUrl();
|
||||
RuleFor(c => c.ProwlarrUrl).IsValidUrl();
|
||||
RuleFor(c => c.ApiKey).NotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public class ListenarrSettings : IApplicationSettings
|
||||
{
|
||||
private static readonly ListenarrSettingsValidator Validator = new ListenarrSettingsValidator();
|
||||
|
||||
public ListenarrSettings()
|
||||
{
|
||||
ProwlarrUrl = "http://localhost:9696";
|
||||
BaseUrl = "http://localhost:5000";
|
||||
SyncLevel = (int)ApplicationSyncLevel.FullSync; // default reasonable behavior
|
||||
}
|
||||
|
||||
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "URL of Prowlarr server as Listenarr sees it, including http:// or https://, and port if needed")]
|
||||
public string ProwlarrUrl { get; set; }
|
||||
|
||||
[FieldDefinition(1, Label = "Listenarr Server", HelpText = "URL of Listenarr server, including http:// or https://, and port if needed")]
|
||||
public string BaseUrl { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "API Key", Privacy = PrivacyLevel.ApiKey, HelpText = "API Key for Listenarr")]
|
||||
public string ApiKey { get; set; }
|
||||
|
||||
[FieldDefinition(3, Type = FieldType.Select, SelectOptions = typeof(ApplicationSyncLevel), Label = "Sync Level", HelpText = "How should Prowlarr sync indexers to Listenarr")]
|
||||
public int SyncLevel { get; set; }
|
||||
|
||||
[FieldDefinition(4, Label = "Sync Categories", Advanced = true, HelpText = "Sync audiobook categories to Listenarr (must match Listenarr's category schema)")]
|
||||
public IEnumerable<int> SyncCategories { get; set; }
|
||||
|
||||
[FieldDefinition(5, Type = FieldType.TagSelect, SelectOptionsProviderAction = "getTags", Label = "Tags", HelpText = "Only add indexers with these tags to Listenarr")]
|
||||
public IEnumerable<int> Tags { get; set; }
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
116
src/NzbDrone.Core/Applications/Listenarr/ListenarrV1Proxy.cs
Normal file
116
src/NzbDrone.Core/Applications/Listenarr/ListenarrV1Proxy.cs
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using Newtonsoft.Json;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Serializer;
|
||||
|
||||
namespace NzbDrone.Core.Applications.Listenarr
|
||||
{
|
||||
public interface IListenarrV1Proxy
|
||||
{
|
||||
ListenarrStatus GetStatus(ListenarrSettings settings);
|
||||
List<ListenarrIndexer> GetIndexers(ListenarrSettings settings);
|
||||
ListenarrIndexer AddIndexer(ListenarrIndexer indexer, ListenarrSettings settings);
|
||||
void UpdateIndexer(ListenarrIndexer indexer, ListenarrSettings settings);
|
||||
void RemoveIndexer(int indexerId, ListenarrSettings settings);
|
||||
List<ListenarrTag> GetTags(ListenarrSettings settings);
|
||||
}
|
||||
|
||||
public class ListenarrV1Proxy : IListenarrV1Proxy
|
||||
{
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public ListenarrV1Proxy(IHttpClient httpClient, Logger logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public ListenarrStatus GetStatus(ListenarrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, "/api/system/status", HttpMethod.Get);
|
||||
return Execute<ListenarrStatus>(request);
|
||||
}
|
||||
|
||||
public List<ListenarrIndexer> GetIndexers(ListenarrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, "/api/indexer", HttpMethod.Get);
|
||||
return Execute<List<ListenarrIndexer>>(request);
|
||||
}
|
||||
|
||||
public ListenarrIndexer AddIndexer(ListenarrIndexer indexer, ListenarrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, "/api/indexer", HttpMethod.Post);
|
||||
request.SetContent(indexer.ToJson());
|
||||
request.ContentSummary = indexer.ToJson(Formatting.None);
|
||||
return Execute<ListenarrIndexer>(request);
|
||||
}
|
||||
|
||||
public void UpdateIndexer(ListenarrIndexer indexer, ListenarrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, $"/api/indexer/{indexer.Id}", HttpMethod.Put);
|
||||
request.SetContent(indexer.ToJson());
|
||||
request.ContentSummary = indexer.ToJson(Formatting.None);
|
||||
Execute<ListenarrIndexer>(request);
|
||||
}
|
||||
|
||||
public void RemoveIndexer(int indexerId, ListenarrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, $"/api/indexer/{indexerId}", HttpMethod.Delete);
|
||||
_httpClient.Execute(request);
|
||||
}
|
||||
|
||||
public List<ListenarrTag> GetTags(ListenarrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, "/api/tag", HttpMethod.Get);
|
||||
return Execute<List<ListenarrTag>>(request);
|
||||
}
|
||||
|
||||
private HttpRequest BuildRequest(ListenarrSettings settings, string resource, HttpMethod method)
|
||||
{
|
||||
var baseUrl = settings.BaseUrl.TrimEnd('/');
|
||||
|
||||
var request = new HttpRequestBuilder(baseUrl)
|
||||
.Resource(resource)
|
||||
.Accept(HttpAccept.Json)
|
||||
.SetHeader("X-Api-Key", settings.ApiKey)
|
||||
.Build();
|
||||
|
||||
request.Headers.ContentType = "application/json";
|
||||
request.Method = method;
|
||||
request.AllowAutoRedirect = true;
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
private T Execute<T>(HttpRequest request)
|
||||
where T : new()
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = _httpClient.Execute(request);
|
||||
|
||||
if ((int)response.StatusCode >= 300)
|
||||
{
|
||||
throw new HttpException(response);
|
||||
}
|
||||
|
||||
return Json.Deserialize<T>(response.Content);
|
||||
}
|
||||
catch (HttpException ex)
|
||||
{
|
||||
if (ex.Response != null && ex.Response.StatusCode == HttpStatusCode.Unauthorized)
|
||||
{
|
||||
_logger.Error(ex, "API Key is invalid");
|
||||
throw new ApplicationException("API Key is invalid");
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue