fixup! fixup! fixup! fixup! fixup! fixup! fixup! Add initial Blazor Server project

recyclarr
Robert Dailey 3 years ago
parent 11dcf1097d
commit 24c41ff4f4

@ -6,7 +6,7 @@
ValueChanged="OnSelectionChanged"
Label="@Label"
Class="mt-3 mt-sm-0">
@foreach (var instance in ConfigProvider.Configs)
@foreach (var instance in Configs)
{
<MudSelectItem Value="@instance">@instance.BaseUrl</MudSelectItem>
}

@ -17,7 +17,7 @@ namespace Recyclarr.Components
public ILocalStorageService LocalStorage { get; set; } = default!;
[Inject]
public IConfigProvider<TConfig> ConfigProvider { get; set; } = default!;
public ICollection<TConfig> Configs { get; set; } = default!;
[Parameter]
public string Label { get; set; } = "Select Server";
@ -30,22 +30,14 @@ namespace Recyclarr.Components
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
if (!ConfigProvider.IsActiveValid())
{
_afterRenderActions.Enqueue(LoadSelected);
}
else
{
await SetSelected(ConfigProvider.Active, false);
}
_afterRenderActions.Enqueue(LoadSelected);
}
private async Task LoadSelected()
{
var savedSelection = await LocalStorage.GetItemAsync<string>("selectedInstance");
var instanceToSelect = ConfigProvider.Configs.FirstOrDefault(c => c.BaseUrl == savedSelection);
await SetSelected(instanceToSelect ?? ConfigProvider.Configs.FirstOrDefault(), false);
var instanceToSelect = Configs.FirstOrDefault(c => c.BaseUrl == savedSelection);
await SetSelected(instanceToSelect ?? Configs.FirstOrDefault(), false);
}
private async Task SaveSelected()

@ -1,7 +1,6 @@
using System.IO.Abstractions;
using Autofac;
using BlazorPro.BlazorSize;
using Recyclarr.Code;
using Recyclarr.Code.Radarr;
using Recyclarr.Code.Settings;
using Recyclarr.Code.Settings.Persisters;
@ -41,6 +40,9 @@ namespace Recyclarr
builder.RegisterType<ConfigPersister<RadarrConfiguration>>()
.As<IConfigPersister<RadarrConfiguration>>()
.WithParameter(new NamedParameter("filename", "radarr.json"));
builder.Register(c => c.Resolve<IConfigPersister<RadarrConfiguration>>().Load())
.InstancePerLifetimeScope();
}
}
}

@ -6,7 +6,6 @@ using JetBrains.Annotations;
using Microsoft.AspNetCore.Components;
using MudBlazor;
using Recyclarr.Code.Radarr;
using Recyclarr.Code.Settings.Persisters;
using Recyclarr.Components;
using TrashLib.Config;
using TrashLib.Radarr.Config;
@ -30,7 +29,7 @@ namespace Recyclarr.Pages.Radarr.CustomFormats
public IConfigPersister<RadarrConfiguration> ConfigPersister { get; set; } = default!;
[Inject]
public IConfigProvider<RadarrConfiguration> ConfigProvider { get; set; } = default!;
public ICollection<RadarrConfiguration> Configs { get; set; } = default!;
private bool? SelectAllCheckbox { get; set; } = false;
private List<string> ChosenCustomFormatIds => _currentSelection.Select(cf => cf.Item.TrashIds.First()).ToList();
@ -94,7 +93,7 @@ namespace Recyclarr.Pages.Radarr.CustomFormats
.ToList();
}
ConfigPersister.Save(ConfigProvider.Configs);
ConfigPersister.Save(Configs);
}
private IEnumerable<SelectableCustomFormat> GetSelected() => _currentSelection.Where(i => i.Selected);

@ -1,10 +1,11 @@
@page "/radarr/quality-profiles"
@using TrashLib.Radarr.CustomFormat.Api
@using Recyclarr.Components
@using TrashLib.Radarr.Config
@using Newtonsoft.Json.Linq
@using Recyclarr.Code.Settings.Persisters
@using TrashLib.Config
@using Recyclarr.Components
@using Newtonsoft.Json.Linq
@using Flurl.Http
@using Newtonsoft.Json
<div class="d-flex mb-4 flex-column flex-sm-row">
@ -14,70 +15,139 @@
SelectionChanged="@OnSelectedInstanceChanged" />
</div>
<MudPaper Class="d-flex" Outlined="true">
@* <MudSelect @bind-Value="@_selectedProfile"> *@
@* @foreach (var profile in _profiles) *@
@* { *@
@* <MudSelectItem></MudSelectItem> *@
@* } *@
@* </MudSelect> *@
@if (_profiles == null || _profiles.Count == 0)
<MudPaper Class="d-flex flex-column pa-4" Outlined="true">
@if (_exception != null)
{
<p>This instance has no quality profiles</p>
<MudContainer>
<MudText Class="mb-2">
A failure occurred while trying to get quality profiles from Radarr:
</MudText>
<MudText Class="mb-2" Color="Color.Error" Style="overflow: hidden">
@_exception.Message
</MudText>
<MudButton Class="mud-theme-primary my-2" OnClick="@ForceReload">Retry</MudButton>
</MudContainer>
return;
}
else
@if (_loading)
{
<MudList T="JObject" Clickable="true" @bind-SelectedItem="@SelectedListItem">
@foreach (var profile in _profiles)
{
<MudListItem >@((string) profile["name"])</MudListItem>
}
</MudList>
<MudContainer Class="d-flex flex-column align-center">
<MudText Align="Align.Center">Loading quality profiles...</MudText>
<MudProgressCircular Class="mt-2" Color="Color.Primary" Indeterminate="true" />
</MudContainer>
return;
}
@if (_profiles != null)
@if (_profiles.Count == 0)
{
<MudText Typo="Typo.body1" Class="mx-auto">
This instance has no quality profiles
</MudText>
return;
}
<MudSelect @bind-Value="@_selectedProfileId" Class="mb-2" Style="width: auto">
@foreach (var profile in _profiles)
{
<MudSimpleTable>
<thead>
<tr>
<th>Custom Format</th>
<th>New Score</th>
<th>Score in Radarr</th>
</tr>
</thead>
<tbody>
<MudSelectItem @key="profile" Value="@((int) profile["id"])">
@((string) profile["name"])
</MudSelectItem>
}
</MudSelect>
@* <MudList @ref="@_profileSelectList" *@
@* T="JObject" *@
@* Clickable="true" *@
@* @bind-SelectedItem="@SelectedListItem" *@
@* Class="mr-2" /> *@
<MudSimpleTable Dense="true">
<thead>
<tr>
<th>Custom Format</th>
<th>Manual Score</th>
<th>Guide Score</th>
<th>Radarr Score</th>
</tr>
</thead>
<tbody>
@if (SelectedProfile != null)
{
@foreach (var formatItem in SelectedProfile["formatItems"].Children<JObject>())
{
<tr>
<td></td>
<td></td>
<td></td>
<td>@((int)formatItem["score"])</td>
</tr>
}
}
</tbody>
</tbody>
</MudSimpleTable>
}
}
</MudSimpleTable>
</MudPaper>
@code {
[Inject]
public IQualityProfileService ProfileService { get; set; } = default!;
public Func<string, IQualityProfileService> ProfileServiceFactory { get; set; } = default!;
[Inject]
public IConfigPersister<RadarrConfiguration> ConfigPersister { get; set; } = default!;
[Inject]
public IConfigProvider<RadarrConfiguration> ConfigProvider { get; set; } = default!;
public ICollection<RadarrConfiguration> Configs { get; set; } = default!;
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
}
private async Task OnSelectedInstanceChanged(RadarrConfiguration? obj)
private async Task OnSelectedInstanceChanged(RadarrConfiguration? activeConfig)
{
_profiles = await ProfileService.GetQualityProfiles();
try
{
_activeConfig = activeConfig;
_exception = null;
_loading = true;
_profiles.Clear();
if (activeConfig != null)
{
_profiles.AddRange(await ProfileServiceFactory(activeConfig.BuildUrl()).GetQualityProfiles());
}
SelectedProfile = _profiles.FirstOrDefault();
}
catch (FlurlHttpException e)
{
_exception = e;
}
_loading = false;
}
private RadarrConfiguration? _activeConfig;
private ServerSelector<RadarrConfiguration>? _serverSelector;
private List<JObject>? _profiles;
public MudListItem? SelectedListItem { get; set; }
private readonly List<JObject> _profiles = new();
private Exception? _exception;
private bool _loading;
private async Task ForceReload()
{
await OnSelectedInstanceChanged(_activeConfig);
}
private JObject? SelectedProfile
{
get => _profiles.FirstOrDefault(p => (int) p["id"] == _selectedProfileId);
set => _selectedProfileId = (int?) value?["id"];
}
private int? _selectedProfileId;
}

@ -3,14 +3,14 @@
<MudButton Class="mud-theme-primary mb-4" OnClick="@OnAddServer">Add Server</MudButton>
<MudPaper Outlined="true" Class="d-flex justify-center pa-4">
@if (ConfigProvider.Configs.Count == 0)
@if (Configs.Count == 0)
{
<MudText Typo="Typo.h6">No Servers Configured</MudText>
}
else
{
<MudGrid>
@foreach (var instance in ConfigProvider.Configs)
@foreach (var instance in Configs)
{
<MudItem xs="12" sm="6">
<MudCard Elevation="3" Style="background-color: #2f2e3a" Outlined="true">

@ -1,8 +1,8 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Components;
using MudBlazor;
using Recyclarr.Code.Settings.Persisters;
using TrashLib.Config;
using TrashLib.Radarr.Config;
@ -18,7 +18,7 @@ namespace Recyclarr.Pages.Radarr.Servers
public IConfigPersister<RadarrConfiguration> ConfigPersister { get; set; } = default!;
[Inject]
public IConfigProvider<RadarrConfiguration> ConfigProvider { get; set; } = default!;
public ICollection<RadarrConfiguration> Configs { get; set; } = default!;
private async Task<bool> ShowEditServerModal(string title, ServiceConfiguration instance)
{
@ -50,7 +50,7 @@ namespace Recyclarr.Pages.Radarr.Servers
var item = new RadarrConfiguration();
if (await ShowEditServerModal("Add Server", item))
{
ConfigProvider.Configs.Add(item);
Configs.Add(item);
SaveServers();
}
}
@ -63,7 +63,7 @@ namespace Recyclarr.Pages.Radarr.Servers
private void SaveServers()
{
ConfigPersister.Save(ConfigProvider.Configs);
ConfigPersister.Save(Configs);
}
private async Task OnDelete(RadarrConfiguration item)
@ -76,7 +76,7 @@ namespace Recyclarr.Pages.Radarr.Servers
if (shouldDelete == true)
{
ConfigProvider.Configs.Remove(item);
Configs.Remove(item);
SaveServers();
}
}

@ -5,6 +5,8 @@
@* Layout = null; *@
@* } *@
@* ReSharper disable Html.PathError *@
<!DOCTYPE html>
<html lang="en">
<head>

@ -35,9 +35,9 @@ namespace Trash.Tests.Config
Justification = "YamlDotNet requires this type to be public so it may access it")]
public class TestConfig : IServiceConfiguration
{
public string ServiceId => "";
public string BaseUrl => "";
public string ApiKey => "";
public string BuildUrl() => throw new NotImplementedException();
}
[Test]
@ -55,20 +55,8 @@ namespace Trash.Tests.Config
fs.File.OpenText(Arg.Any<string>())
.Returns(MockYaml(1, 2), MockYaml(3));
var provider = Substitute.For<IConfigProvider<SonarrConfiguration>>();
// var objectFactory = Substitute.For<IObjectFactory>();
// objectFactory.Create(Arg.Any<Type>())
// .Returns(t => Substitute.For(new[] {(Type)t[0]}, Array.Empty<object>()));
var actualActiveConfigs = new List<SonarrConfiguration>();
provider.Active.Returns(
#pragma warning disable NS1004
Arg.Do<SonarrConfiguration>(a => actualActiveConfigs.Add(a)));
#pragma warning restore NS1004
var validator = Substitute.For<IValidator<SonarrConfiguration>>();
var loader =
new ConfigurationLoader<SonarrConfiguration>(provider, fs, new DefaultObjectFactory(), validator);
var loader = new ConfigurationLoader<SonarrConfiguration>(fs, new DefaultObjectFactory(), validator);
var fakeFiles = new List<string>
{
@ -86,16 +74,13 @@ namespace Trash.Tests.Config
var actual = loader.LoadMany(fakeFiles, "sonarr").ToList();
actual.Should().BeEquivalentTo(expected);
actualActiveConfigs.Should().BeEquivalentTo(expected, op => op.WithoutStrictOrdering());
}
[Test]
public void Parse_using_stream()
{
var validator = Substitute.For<IValidator<SonarrConfiguration>>();
var configLoader = new ConfigurationLoader<SonarrConfiguration>(
Substitute.For<IConfigProvider<SonarrConfiguration>>(),
Substitute.For<IFileSystem>(),
var configLoader = new ConfigurationLoader<SonarrConfiguration>(Substitute.For<IFileSystem>(),
new DefaultObjectFactory(),
validator);
@ -135,9 +120,7 @@ namespace Trash.Tests.Config
public void Throw_when_validation_fails()
{
var validator = Substitute.For<IValidator<TestConfig>>();
var configLoader = new ConfigurationLoader<TestConfig>(
Substitute.For<IConfigProvider<TestConfig>>(),
Substitute.For<IFileSystem>(),
var configLoader = new ConfigurationLoader<TestConfig>(Substitute.For<IFileSystem>(),
new DefaultObjectFactory(),
validator);
@ -160,9 +143,7 @@ fubar:
public void Validation_success_does_not_throw()
{
var validator = Substitute.For<IValidator<TestConfig>>();
var configLoader = new ConfigurationLoader<TestConfig>(
Substitute.For<IConfigProvider<TestConfig>>(),
Substitute.For<IFileSystem>(),
var configLoader = new ConfigurationLoader<TestConfig>(Substitute.For<IFileSystem>(),
new DefaultObjectFactory(),
validator);

@ -15,18 +15,15 @@ namespace Trash.Config
public class ConfigurationLoader<T> : IConfigurationLoader<T>
where T : IServiceConfiguration
{
private readonly IConfigProvider<T> _configProvider;
private readonly IDeserializer _deserializer;
private readonly IFileSystem _fileSystem;
private readonly IValidator<T> _validator;
public ConfigurationLoader(
IConfigProvider<T> configProvider,
IFileSystem fileSystem,
IObjectFactory objectFactory,
IValidator<T> validator)
{
_configProvider = configProvider;
_fileSystem = fileSystem;
_validator = validator;
_deserializer = new DeserializerBuilder()
@ -84,11 +81,7 @@ namespace Trash.Config
public IEnumerable<T> LoadMany(IEnumerable<string> configFiles, string configSection)
{
foreach (var config in configFiles.SelectMany(file => Load(file, configSection)))
{
_configProvider.Active = config;
yield return config;
}
return configFiles.SelectMany(file => Load(file, configSection));
}
}
}

@ -24,7 +24,6 @@ namespace TrashLib.Tests.Cache
{
Filesystem = fs ?? Substitute.For<IFileSystem>();
StoragePath = Substitute.For<ICacheStoragePath>();
ServerInfo = Substitute.For<IServerInfo>();
JsonSettings = new JsonSerializerSettings
{
Formatting = Formatting.Indented,
@ -34,17 +33,23 @@ namespace TrashLib.Tests.Cache
}
};
// Set up a default for the active config's base URL. This is used to generate part of the path
ServerInfo.BaseUrl.Returns("http://localhost:1234");
Cache = new ServiceCache(Filesystem, StoragePath, ServerInfo, Substitute.For<ILogger>());
Cache = new ServiceCache(Filesystem, StoragePath, Substitute.For<ILogger>());
}
public JsonSerializerSettings JsonSettings { get; }
public ServiceCache Cache { get; }
public IServerInfo ServerInfo { get; }
public ICacheStoragePath StoragePath { get; }
public IFileSystem Filesystem { get; }
public ICacheGuidBuilder MakeGuidBuilder(string baseUrl = "http://localhost:1234")
{
return new CacheGuidBuilder(new TestServiceConfig
{BaseUrl = baseUrl});
}
private class TestServiceConfig : ServiceConfiguration
{
}
}
private class ObjectWithoutAttribute
@ -70,7 +75,7 @@ namespace TrashLib.Tests.Cache
var ctx = new Context();
ctx.Filesystem.File.Exists(Arg.Any<string>()).Returns(false);
var result = ctx.Cache.Load<ObjectWithAttribute>();
var result = ctx.Cache.Load<ObjectWithAttribute>(ctx.MakeGuidBuilder());
result.Should().BeNull();
}
@ -86,7 +91,7 @@ namespace TrashLib.Tests.Cache
ctx.Filesystem.File.ReadAllText(Arg.Any<string>())
.Returns(_ => JsonConvert.SerializeObject(testJson));
var obj = ctx.Cache.Load<ObjectWithAttribute>();
var obj = ctx.Cache.Load<ObjectWithAttribute>(ctx.MakeGuidBuilder());
obj.Should().NotBeNull();
obj!.TestValue.Should().Be("Foo");
@ -98,7 +103,7 @@ namespace TrashLib.Tests.Cache
{
var ctx = new Context();
Action act = () => ctx.Cache.Load<ObjectWithAttributeInvalidChars>();
Action act = () => ctx.Cache.Load<ObjectWithAttributeInvalidChars>(ctx.MakeGuidBuilder());
act.Should()
.Throw<ArgumentException>()
@ -110,7 +115,7 @@ namespace TrashLib.Tests.Cache
{
var ctx = new Context();
Action act = () => ctx.Cache.Load<ObjectWithoutAttribute>();
Action act = () => ctx.Cache.Load<ObjectWithoutAttribute>(ctx.MakeGuidBuilder());
act.Should()
.Throw<ArgumentException>()
@ -122,7 +127,7 @@ namespace TrashLib.Tests.Cache
{
var ctx = new Context();
ctx.StoragePath.Path.Returns("testpath");
ctx.Cache.Save(new ObjectWithAttribute {TestValue = "Foo"});
ctx.Cache.Save(new ObjectWithAttribute {TestValue = "Foo"}, ctx.MakeGuidBuilder());
ctx.Filesystem.File.Received()
.WriteAllText(Arg.Any<string>(), Verify.That<string>(json => json.Should().Contain("\"test_value\"")));
@ -135,7 +140,7 @@ namespace TrashLib.Tests.Cache
ctx.StoragePath.Path.Returns("testpath");
ctx.Cache.Save(new ObjectWithAttribute {TestValue = "Foo"});
ctx.Cache.Save(new ObjectWithAttribute {TestValue = "Foo"}, ctx.MakeGuidBuilder());
var expectedParentDirectory = Path.Combine("testpath", "be8fbc8f");
ctx.Filesystem.Directory.Received().CreateDirectory(expectedParentDirectory);
@ -151,7 +156,7 @@ namespace TrashLib.Tests.Cache
{
var ctx = new Context();
Action act = () => ctx.Cache.Save(new ObjectWithAttributeInvalidChars());
Action act = () => ctx.Cache.Save(new ObjectWithAttributeInvalidChars(), ctx.MakeGuidBuilder());
act.Should()
.Throw<ArgumentException>()
@ -163,7 +168,7 @@ namespace TrashLib.Tests.Cache
{
var ctx = new Context();
Action act = () => ctx.Cache.Save(new ObjectWithoutAttribute());
Action act = () => ctx.Cache.Save(new ObjectWithoutAttribute(), ctx.MakeGuidBuilder());
act.Should()
.Throw<ArgumentException>()
@ -183,12 +188,10 @@ namespace TrashLib.Tests.Cache
ctx.Filesystem.File.ReadAllText(Arg.Do<string>(s => actualPaths.Add(s)))
.Returns(_ => JsonConvert.SerializeObject(testJson));
ctx.Cache.Load<ObjectWithAttribute>();
ctx.Cache.Load<ObjectWithAttribute>(ctx.MakeGuidBuilder());
// Change the active config & base URL so we get a different path
ctx.ServerInfo.BaseUrl.Returns("http://localhost:5678");
ctx.Cache.Load<ObjectWithAttribute>();
ctx.Cache.Load<ObjectWithAttribute>(ctx.MakeGuidBuilder("http://localhost:5678"));
actualPaths.Count.Should().Be(2);
actualPaths.Should().OnlyHaveUniqueItems();
@ -202,7 +205,7 @@ namespace TrashLib.Tests.Cache
ctx.Filesystem.File.ReadAllText(Arg.Any<string>())
.Returns(_ => "");
Action act = () => ctx.Cache.Load<ObjectWithAttribute>();
Action act = () => ctx.Cache.Load<ObjectWithAttribute>(ctx.MakeGuidBuilder());
act.Should().NotThrow();
}

@ -22,9 +22,11 @@ namespace TrashLib.Tests.Radarr.CustomFormat
{
Log = Substitute.For<ILogger>();
ServiceCache = Substitute.For<IServiceCache>();
Persister = new CachePersister(Log, ServiceCache);
GuidBuilder = Substitute.For<ICacheGuidBuilder>();
Persister = new CachePersister(Log, ServiceCache, GuidBuilder);
}
public ICacheGuidBuilder GuidBuilder { get; }
public CachePersister Persister { get; }
public ILogger Log { get; }
public IServiceCache ServiceCache { get; }
@ -49,7 +51,7 @@ namespace TrashLib.Tests.Radarr.CustomFormat
Version = versionToTest,
TrashIdMappings = new List<TrashIdMapping> {new("", "", 5)}
};
ctx.ServiceCache.Load<CustomFormatCache>().Returns(testCfObj);
ctx.ServiceCache.Load<CustomFormatCache>(Arg.Any<ICacheGuidBuilder>()).Returns(testCfObj);
ctx.Persister.Load();
ctx.Persister.CfCache.Should().BeNull();
}
@ -64,7 +66,7 @@ namespace TrashLib.Tests.Radarr.CustomFormat
Version = CustomFormatCache.LatestVersion,
TrashIdMappings = new List<TrashIdMapping> {new("", "", 5)}
};
ctx.ServiceCache.Load<CustomFormatCache>().Returns(testCfObj);
ctx.ServiceCache.Load<CustomFormatCache>(Arg.Any<ICacheGuidBuilder>()).Returns(testCfObj);
ctx.Persister.Load();
ctx.Persister.CfCache.Should().NotBeNull();
}
@ -74,7 +76,7 @@ namespace TrashLib.Tests.Radarr.CustomFormat
{
var ctx = new Context();
var testCfObj = new CustomFormatCache();
ctx.ServiceCache.Load<CustomFormatCache>().Returns(testCfObj);
ctx.ServiceCache.Load<CustomFormatCache>(Arg.Any<ICacheGuidBuilder>()).Returns(testCfObj);
ctx.Persister.Load();
ctx.Persister.CfCache.Should().BeSameAs(testCfObj);
@ -93,12 +95,12 @@ namespace TrashLib.Tests.Radarr.CustomFormat
{
var ctx = new Context();
var testCfObj = new CustomFormatCache();
ctx.ServiceCache.Load<CustomFormatCache>().Returns(testCfObj);
ctx.ServiceCache.Load<CustomFormatCache>(Arg.Any<ICacheGuidBuilder>()).Returns(testCfObj);
ctx.Persister.Load();
ctx.Persister.Save();
ctx.ServiceCache.Received().Save(Arg.Is(testCfObj));
ctx.ServiceCache.Received().Save(Arg.Is(testCfObj), Arg.Any<ICacheGuidBuilder>());
}
[Test]
@ -106,7 +108,7 @@ namespace TrashLib.Tests.Radarr.CustomFormat
{
var ctx = new Context();
ctx.Persister.Save();
ctx.ServiceCache.DidNotReceive().Save(Arg.Any<object>());
ctx.ServiceCache.DidNotReceive().Save(Arg.Any<object>(), Arg.Any<ICacheGuidBuilder>());
}
[Test]
@ -119,7 +121,7 @@ namespace TrashLib.Tests.Radarr.CustomFormat
{
TrashIdMappings = new List<TrashIdMapping> {new("", "") {CustomFormatId = 5}}
};
ctx.ServiceCache.Load<CustomFormatCache>().Returns(testCfObj);
ctx.ServiceCache.Load<CustomFormatCache>(Arg.Any<ICacheGuidBuilder>()).Returns(testCfObj);
ctx.Persister.Load();
// Update with new cached items

@ -4,7 +4,6 @@ using System.Collections.ObjectModel;
using Newtonsoft.Json.Linq;
using NSubstitute;
using NUnit.Framework;
using TrashLib.Config;
using TrashLib.Radarr.Config;
using TrashLib.Radarr.CustomFormat.Api;
using TrashLib.Radarr.CustomFormat.Models;
@ -17,68 +16,62 @@ namespace TrashLib.Tests.Radarr.CustomFormat.Processors
[Parallelizable(ParallelScope.All)]
public class PersistenceProcessorTest
{
private class Context
{
public Context()
{
Steps = Substitute.For<IPersistenceProcessorSteps>();
GuideCfs = Array.Empty<ProcessedCustomFormatData>();
DeletedCfsInCache = new Collection<TrashIdMapping>();
ProfileScores = new Dictionary<string, QualityProfileCustomFormatScoreMapping>();
Processor = new PersistenceProcessor(
_ => Substitute.For<ICustomFormatService>(),
_ => Substitute.For<IQualityProfileService>(),
() => Steps);
}
public PersistenceProcessor Processor { get; }
public Dictionary<string, QualityProfileCustomFormatScoreMapping> ProfileScores { get; }
public Collection<TrashIdMapping> DeletedCfsInCache { get; }
public ProcessedCustomFormatData[] GuideCfs { get; }
public IPersistenceProcessorSteps Steps { get; }
}
[Test]
public void Custom_formats_are_deleted_if_deletion_option_is_enabled_in_config()
{
var steps = Substitute.For<IPersistenceProcessorSteps>();
var cfApi = Substitute.For<ICustomFormatService>();
var qpApi = Substitute.For<IQualityProfileService>();
var configProvider = Substitute.For<IConfigProvider<RadarrConfiguration>>();
configProvider.Active.Returns(new RadarrConfiguration {DeleteOldCustomFormats = true});
var guideCfs = Array.Empty<ProcessedCustomFormatData>();
var deletedCfsInCache = new Collection<TrashIdMapping>();
var profileScores = new Dictionary<string, QualityProfileCustomFormatScoreMapping>();
var processor = new PersistenceProcessor(cfApi, qpApi, () => steps);
processor.PersistCustomFormats(guideCfs, deletedCfsInCache, profileScores);
var config = new RadarrConfiguration {DeleteOldCustomFormats = true};
var ctx = new Context();
steps.JsonTransactionStep.Received().RecordDeletions(Arg.Is(deletedCfsInCache), Arg.Any<List<JObject>>());
ctx.Processor.PersistCustomFormats(config, ctx.GuideCfs, ctx.DeletedCfsInCache, ctx.ProfileScores);
ctx.Steps.JsonTransactionStep.Received()
.RecordDeletions(Arg.Is(ctx.DeletedCfsInCache), Arg.Any<List<JObject>>());
}
[Test]
public void Custom_formats_are_not_deleted_if_deletion_option_is_disabled_in_config()
{
var steps = Substitute.For<IPersistenceProcessorSteps>();
var cfApi = Substitute.For<ICustomFormatService>();
var qpApi = Substitute.For<IQualityProfileService>();
var config = new RadarrConfiguration {DeleteOldCustomFormats = false};
var ctx = new Context();
var configProvider = Substitute.For<IConfigProvider<RadarrConfiguration>>();
configProvider.Active.Returns(new RadarrConfiguration {DeleteOldCustomFormats = false});
var guideCfs = Array.Empty<ProcessedCustomFormatData>();
var deletedCfsInCache = Array.Empty<TrashIdMapping>();
var profileScores = new Dictionary<string, QualityProfileCustomFormatScoreMapping>();
var processor = new PersistenceProcessor(cfApi, qpApi, () => steps);
processor.PersistCustomFormats(guideCfs, deletedCfsInCache, profileScores);
steps.JsonTransactionStep.DidNotReceive()
ctx.Processor.PersistCustomFormats(config, ctx.GuideCfs, ctx.DeletedCfsInCache, ctx.ProfileScores);
ctx.Steps.JsonTransactionStep.DidNotReceive()
.RecordDeletions(Arg.Any<IEnumerable<TrashIdMapping>>(), Arg.Any<List<JObject>>());
}
[Test]
public void Different_active_configuration_is_properly_used()
{
var steps = Substitute.For<IPersistenceProcessorSteps>();
var cfApi = Substitute.For<ICustomFormatService>();
var qpApi = Substitute.For<IQualityProfileService>();
var configProvider = Substitute.For<IConfigProvider<RadarrConfiguration>>();
var guideCfs = Array.Empty<ProcessedCustomFormatData>();
var deletedCfsInCache = Array.Empty<TrashIdMapping>();
var profileScores = new Dictionary<string, QualityProfileCustomFormatScoreMapping>();
var processor = new PersistenceProcessor(cfApi, qpApi, () => steps);
var ctx = new Context();
configProvider.Active = new RadarrConfiguration {DeleteOldCustomFormats = false};
processor.PersistCustomFormats(guideCfs, deletedCfsInCache, profileScores);
var config = new RadarrConfiguration {DeleteOldCustomFormats = false};
ctx.Processor.PersistCustomFormats(config, ctx.GuideCfs, ctx.DeletedCfsInCache, ctx.ProfileScores);
configProvider.Active = new RadarrConfiguration {DeleteOldCustomFormats = true};
processor.PersistCustomFormats(guideCfs, deletedCfsInCache, profileScores);
config = new RadarrConfiguration {DeleteOldCustomFormats = true};
ctx.Processor.PersistCustomFormats(config, ctx.GuideCfs, ctx.DeletedCfsInCache, ctx.ProfileScores);
steps.JsonTransactionStep.Received(1)
ctx.Steps.JsonTransactionStep.Received(1)
.RecordDeletions(Arg.Any<IEnumerable<TrashIdMapping>>(), Arg.Any<List<JObject>>());
}
}

@ -5,14 +5,12 @@ namespace TrashLib.Cache
{
public class CacheAutofacModule : Module
{
// Clients must register their own implementation of ICacheStoragePath
protected override void Load(ContainerBuilder builder)
{
builder.RegisterModule<ConfigAutofacModule>();
builder.RegisterGeneric(typeof(CacheGuidBuilder<>))
.As<ICacheGuidBuilder>();
// Clients must register their own implementation of ICacheStoragePath
builder.RegisterType<CacheGuidBuilder>().As<ICacheGuidBuilder>();
builder.RegisterType<ServiceCache>().As<IServiceCache>();
}
}

@ -0,0 +1,25 @@
using System.Data.HashFunction.FNV;
using System.Text;
using TrashLib.Config;
namespace TrashLib.Cache
{
internal class CacheGuidBuilder : ICacheGuidBuilder
{
private readonly string _baseUrl;
private readonly IFNV1a _hash;
public CacheGuidBuilder(IServiceConfiguration config)
{
_baseUrl = config.BaseUrl;
_hash = FNV1aFactory.Instance.Create(FNVConfig.GetPredefinedConfig(32));
}
public string MakeGuid()
{
return _hash
.ComputeHash(Encoding.ASCII.GetBytes(_baseUrl))
.AsHexString();
}
}
}

@ -0,0 +1,7 @@
namespace TrashLib.Cache
{
public interface ICacheGuidBuilder
{
string MakeGuid();
}
}

@ -2,7 +2,7 @@
{
public interface IServiceCache
{
T? Load<T>() where T : class;
void Save<T>(T obj) where T : class;
T? Load<T>(ICacheGuidBuilder guidBuilder) where T : class;
void Save<T>(T obj, ICacheGuidBuilder guidBuilder) where T : class;
}
}

@ -1,67 +1,36 @@
using System;
using System.Data.HashFunction.FNV;
using System.IO;
using System.IO.Abstractions;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using Serilog;
using TrashLib.Config;
namespace TrashLib.Cache
{
public interface ICacheGuidBuilder
{
string MakeGuid();
}
internal class CacheGuidBuilder<TConfig> : ICacheGuidBuilder
where TConfig : IServiceConfiguration
{
private readonly IFNV1a _hash;
private readonly IConfigProvider<TConfig> _configProvider;
public CacheGuidBuilder(IConfigProvider<TConfig> configProvider)
{
_configProvider = configProvider;
_hash = FNV1aFactory.Instance.Create(FNVConfig.GetPredefinedConfig(32));
}
public string MakeGuid()
{
return _hash
.ComputeHash(Encoding.ASCII.GetBytes(_configProvider.Active.BaseUrl))
.AsHexString();
}
}
internal class ServiceCache : IServiceCache
{
private static readonly Regex AllowedObjectNameCharacters = new(@"^[\w-]+$", RegexOptions.Compiled);
private readonly IFileSystem _fileSystem;
private readonly ICacheGuidBuilder _guidBuilder;
private readonly ICacheStoragePath _storagePath;
public ServiceCache(
IFileSystem fileSystem,
ICacheStoragePath storagePath,
ICacheGuidBuilder guidBuilder,
ILogger log)
{
_fileSystem = fileSystem;
_storagePath = storagePath;
_guidBuilder = guidBuilder;
Log = log;
}
private ILogger Log { get; }
public T? Load<T>() where T : class
public T? Load<T>(ICacheGuidBuilder guidBuilder) where T : class
{
var path = PathFromAttribute<T>();
var path = PathFromAttribute<T>(guidBuilder);
if (!_fileSystem.File.Exists(path))
{
return null;
@ -81,9 +50,9 @@ namespace TrashLib.Cache
return null;
}
public void Save<T>(T obj) where T : class
public void Save<T>(T obj, ICacheGuidBuilder guidBuilder) where T : class
{
var path = PathFromAttribute<T>();
var path = PathFromAttribute<T>(guidBuilder);
_fileSystem.Directory.CreateDirectory(Path.GetDirectoryName(path));
_fileSystem.File.WriteAllText(path, JsonConvert.SerializeObject(obj, new JsonSerializerSettings
{
@ -106,7 +75,7 @@ namespace TrashLib.Cache
return attribute.Name;
}
private string PathFromAttribute<T>()
private string PathFromAttribute<T>(ICacheGuidBuilder guidBuilder)
{
var objectName = GetCacheObjectNameAttribute<T>();
if (!AllowedObjectNameCharacters.IsMatch(objectName))
@ -114,7 +83,7 @@ namespace TrashLib.Cache
throw new ArgumentException($"Object name '{objectName}' has unacceptable characters");
}
return Path.Combine(_storagePath.Path, _guidBuilder.MakeGuid(), objectName + ".json");
return Path.Combine(_storagePath.Path, guidBuilder.MakeGuid(), objectName + ".json");
}
}
}

@ -9,12 +9,6 @@ namespace TrashLib.Config
{
protected override void Load(ContainerBuilder builder)
{
builder.RegisterGeneric(typeof(GenericConfigProvider<>))
.As(typeof(IConfigProvider<>))
.InstancePerLifetimeScope();
builder.RegisterGeneric(typeof(ServerInfo<>)).As(typeof(IServerInfo<>));
builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly())
.AsClosedTypesOf(typeof(IValidator<>))
.AsImplementedInterfaces();

@ -2,8 +2,8 @@ namespace TrashLib.Config
{
public interface IServiceConfiguration
{
string ServiceId { get; }
string BaseUrl { get; }
string ApiKey { get; }
string BuildUrl();
}
}

@ -1,9 +1,19 @@
namespace TrashLib.Config
using Flurl;
namespace TrashLib.Config
{
public abstract class ServiceConfiguration : IServiceConfiguration
{
public abstract string ServiceId { get; }
public string BaseUrl { get; set; } = "";
public string ApiKey { get; set; } = "";
// This may need to be specialized in subclasses later.
// For now, both Sonarr and Radarr share the same URL structure.
public virtual string BuildUrl()
{
return BaseUrl
.AppendPathSegment("api/v3")
.SetQueryParams(new {apikey = ApiKey});
}
}
}

@ -11,7 +11,6 @@ namespace TrashLib.Radarr.Config
public QualityDefinitionConfig? QualityDefinition { get; init; }
public List<CustomFormatConfig> CustomFormats { get; set; } = new();
public bool DeleteOldCustomFormats { get; init; }
public override string ServiceId => "radarr";
}
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]

@ -3,22 +3,18 @@ using System.Threading.Tasks;
using Flurl;
using Flurl.Http;
using Newtonsoft.Json.Linq;
using TrashLib.Config;
using TrashLib.Radarr.Config;
using TrashLib.Radarr.CustomFormat.Models;
namespace TrashLib.Radarr.CustomFormat.Api
{
internal class CustomFormatService : ICustomFormatService
{
private readonly IServerInfo<RadarrConfiguration> _serverInfo;
public CustomFormatService(IServerInfo<RadarrConfiguration> serverInfo)
public CustomFormatService(string baseUrl)
{
_serverInfo = serverInfo;
BaseUrl = baseUrl;
}
private string BaseUrl => _serverInfo.BuildUrl();
private string BaseUrl { get; }
public async Task<List<JObject>> GetCustomFormats()
{

@ -0,0 +1,22 @@
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace TrashLib.Radarr.CustomFormat.Api.Models
{
public class QualityProfileData
{
[JsonExtensionData] private IDictionary<string, JToken> _extraJson;
public int Id { get; set; }
public string Name { get; set; }
public List<FormatItemData> FormatItems { get; set; }
public class FormatItemData
{
// public int Format { get; set; }
public string Name { get; set; }
public int Score { get; set; }
}
}
}

@ -3,32 +3,28 @@ using System.Threading.Tasks;
using Flurl;
using Flurl.Http;
using Newtonsoft.Json.Linq;
using TrashLib.Config;
using TrashLib.Radarr.Config;
namespace TrashLib.Radarr.CustomFormat.Api
{
internal class QualityProfileService : IQualityProfileService
{
private readonly IServerInfo<RadarrConfiguration> _serverInfo;
private readonly string _baseUrl;
public QualityProfileService(IServerInfo<RadarrConfiguration> serverInfo)
public QualityProfileService(string baseUrl)
{
_serverInfo = serverInfo;
_baseUrl = baseUrl;
}
private string BaseUrl => _serverInfo.BuildUrl();
public async Task<List<JObject>> GetQualityProfiles()
{
return await BaseUrl
return await _baseUrl
.AppendPathSegment("qualityprofile")
.GetJsonAsync<List<JObject>>();
}
public async Task<JObject> UpdateQualityProfile(JObject profileJson, int id)
{
return await BaseUrl
return await _baseUrl
.AppendPathSegment($"qualityprofile/{id}")
.PutJsonAsync(profileJson)
.ReceiveJson<JObject>();

@ -10,11 +10,13 @@ namespace TrashLib.Radarr.CustomFormat
internal class CachePersister : ICachePersister
{
private readonly IServiceCache _cache;
private readonly ICacheGuidBuilder _guidBuilder;
public CachePersister(ILogger log, IServiceCache cache)
public CachePersister(ILogger log, IServiceCache cache, ICacheGuidBuilder guidBuilder)
{
Log = log;
_cache = cache;
_guidBuilder = guidBuilder;
}
private ILogger Log { get; }
@ -22,7 +24,7 @@ namespace TrashLib.Radarr.CustomFormat
public void Load()
{
CfCache = _cache.Load<CustomFormatCache>();
CfCache = _cache.Load<CustomFormatCache>(_guidBuilder);
// ReSharper disable once ConvertIfStatementToConditionalTernaryExpression
if (CfCache != null)
{
@ -52,7 +54,7 @@ namespace TrashLib.Radarr.CustomFormat
}
Log.Debug("Saving Cache");
_cache.Save(CfCache);
_cache.Save(CfCache, _guidBuilder);
}
public void Update(IEnumerable<ProcessedCustomFormatData> customFormats)

@ -18,19 +18,19 @@ namespace TrashLib.Radarr.CustomFormat.Processors
internal class PersistenceProcessor : IPersistenceProcessor
{
private readonly ICustomFormatService _customFormatService;
private readonly IQualityProfileService _qualityProfileService;
private readonly Func<string, ICustomFormatService> _customFormatServiceFactory;
private readonly Func<string, IQualityProfileService> _qualityProfileServiceFactory;
private readonly Func<IPersistenceProcessorSteps> _stepsFactory;
private IPersistenceProcessorSteps _steps;
public PersistenceProcessor(
ICustomFormatService customFormatService,
IQualityProfileService qualityProfileService,
Func<string, ICustomFormatService> customFormatServiceFactory,
Func<string, IQualityProfileService> qualityProfileServiceFactory,
Func<IPersistenceProcessorSteps> stepsFactory)
{
_customFormatService = customFormatService;
_qualityProfileService = qualityProfileService;
_qualityProfileServiceFactory = qualityProfileServiceFactory;
_stepsFactory = stepsFactory;
_customFormatServiceFactory = customFormatServiceFactory;
_steps = _stepsFactory();
}
@ -54,7 +54,8 @@ namespace TrashLib.Radarr.CustomFormat.Processors
IEnumerable<TrashIdMapping> deletedCfsInCache,
IDictionary<string, QualityProfileCustomFormatScoreMapping> profileScores)
{
var radarrCfs = await _customFormatService.GetCustomFormats();
var customFormatService = _customFormatServiceFactory(config.BuildUrl());
var radarrCfs = await customFormatService.GetCustomFormats();
// Step 1: Match CFs between the guide & Radarr and merge the data. The goal is to retain as much of the
// original data from Radarr as possible. There are many properties in the response JSON that we don't
@ -67,15 +68,14 @@ namespace TrashLib.Radarr.CustomFormat.Processors
_steps.JsonTransactionStep.RecordDeletions(deletedCfsInCache, radarrCfs);
}
var customFormatService = _serviceFactory.CreateService<ICustomFormatService>(config);
// Step 2: For each merged CF, persist it to Radarr via its API. This will involve a combination of updates
// to existing CFs and creation of brand new ones, depending on what's already available in Radarr.
await _steps.CustomFormatCustomFormatApiPersister.Process(_customFormatService,
_steps.JsonTransactionStep.Transactions);
await _steps.CustomFormatCustomFormatApiPersister.Process(
customFormatService, _steps.JsonTransactionStep.Transactions);
// Step 3: Update all quality profiles with the scores from the guide for the uploaded custom formats
await _steps.ProfileQualityProfileApiPersister.Process(_qualityProfileService, profileScores);
await _steps.ProfileQualityProfileApiPersister.Process(
_qualityProfileServiceFactory(config.BuildUrl()), profileScores);
}
}
}

@ -2,26 +2,22 @@
using System.Threading.Tasks;
using Flurl;
using Flurl.Http;
using TrashLib.Config;
using TrashLib.Radarr.Config;
using TrashLib.Radarr.QualityDefinition.Api.Objects;
namespace TrashLib.Radarr.QualityDefinition.Api
{
internal class QualityDefinitionService : IQualityDefinitionService
{
private readonly IServerInfo<RadarrConfiguration> _serverInfo;
private readonly string _baseUrl;
public QualityDefinitionService(IServerInfo<RadarrConfiguration> serverInfo)
public QualityDefinitionService(string baseUrl)
{
_serverInfo = serverInfo;
_baseUrl = baseUrl;
}
private string BaseUrl => _serverInfo.BuildUrl();
public async Task<List<RadarrQualityDefinitionItem>> GetQualityDefinition()
{
return await BaseUrl
return await _baseUrl
.AppendPathSegment("qualitydefinition")
.GetJsonAsync<List<RadarrQualityDefinitionItem>>();
}
@ -29,7 +25,7 @@ namespace TrashLib.Radarr.QualityDefinition.Api
public async Task<IList<RadarrQualityDefinitionItem>> UpdateQualityDefinition(
IList<RadarrQualityDefinitionItem> newQuality)
{
return await BaseUrl
return await _baseUrl
.AppendPathSegment("qualityDefinition/update")
.PutJsonAsync(newQuality)
.ReceiveJson<List<RadarrQualityDefinitionItem>>();

@ -1,6 +1,7 @@
using System;
using Autofac;
using Autofac.Extras.AggregateService;
using TrashLib.Cache;
using TrashLib.Config;
using TrashLib.Radarr.Config;
using TrashLib.Radarr.CustomFormat;
@ -16,20 +17,25 @@ namespace TrashLib.Radarr
{
public class RadarrAutofacModule : Module
{
class ServiceFactory<T>
class CachePersisterFactory
{
private readonly Func<string, T> _factory;
private readonly Func<IServiceConfiguration, ICacheGuidBuilder> _guidBuilderFactory;
private readonly Func<ICacheGuidBuilder, ICachePersister> _persisterFactory;
public ServiceFactory(Func<string, T> factory)
public CachePersisterFactory(
Func<IServiceConfiguration, ICacheGuidBuilder> guidBuilderFactory,
Func<ICacheGuidBuilder, ICachePersister> persisterFactory)
{
_factory = factory;
_guidBuilderFactory = guidBuilderFactory;
_persisterFactory = persisterFactory;
}
public T Create(IServiceConfiguration config)
public ICachePersister Create(IServiceConfiguration config)
{
return _factory()
return _persisterFactory(_guidBuilderFactory(config));
}
}
protected override void Load(ContainerBuilder builder)
{
// Services
@ -50,6 +56,12 @@ namespace TrashLib.Radarr
builder.RegisterType<LocalRepoCustomFormatJsonParser>().As<IRadarrGuideService>();
builder.RegisterType<CachePersister>().As<ICachePersister>();
builder.Register<Func<IServiceConfiguration, ICachePersister>>(c => config =>
{
var guidBuilderFactory = c.Resolve<Func<IServiceConfiguration, ICacheGuidBuilder>>();
return c.Resolve<CachePersister>(TypedParameter.From(guidBuilderFactory(config)));
});
// Guide Processor
// todo: register as singleton to avoid parsing guide multiple times when using 2 or more instances in config
builder.RegisterType<GuideProcessor>().As<IGuideProcessor>();

@ -3,24 +3,22 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using Flurl;
using Flurl.Http;
using TrashLib.Config;
using TrashLib.Sonarr.Api.Objects;
using TrashLib.Sonarr.Config;
namespace TrashLib.Sonarr.Api
{
public class SonarrApi : ISonarrApi
{
private readonly IServerInfo<SonarrConfiguration> _serverInfo;
private readonly string _baseUrl;
public SonarrApi(IServerInfo<SonarrConfiguration> serverInfo)
public SonarrApi(string baseUrl)
{
_serverInfo = serverInfo;
_baseUrl = baseUrl;
}
public async Task<Version> GetVersion()
{
dynamic data = await BaseUrl()
dynamic data = await _baseUrl
.AppendPathSegment("system/status")
.GetJsonAsync();
return new Version(data.version);
@ -28,14 +26,14 @@ namespace TrashLib.Sonarr.Api
public async Task<IList<SonarrTag>> GetTags()
{
return await BaseUrl()
return await _baseUrl
.AppendPathSegment("tag")
.GetJsonAsync<List<SonarrTag>>();
}
public async Task<SonarrTag> CreateTag(string tag)
{
return await BaseUrl()
return await _baseUrl
.AppendPathSegment("tag")
.PostJsonAsync(new {label = tag})
.ReceiveJson<SonarrTag>();
@ -43,21 +41,21 @@ namespace TrashLib.Sonarr.Api
public async Task<IList<SonarrReleaseProfile>> GetReleaseProfiles()
{
return await BaseUrl()
return await _baseUrl
.AppendPathSegment("releaseprofile")
.GetJsonAsync<List<SonarrReleaseProfile>>();
}
public async Task UpdateReleaseProfile(SonarrReleaseProfile profileToUpdate)
{
await BaseUrl()
await _baseUrl
.AppendPathSegment($"releaseprofile/{profileToUpdate.Id}")
.PutJsonAsync(profileToUpdate);
}
public async Task<SonarrReleaseProfile> CreateReleaseProfile(SonarrReleaseProfile newProfile)
{
return await BaseUrl()
return await _baseUrl
.AppendPathSegment("releaseprofile")
.PostJsonAsync(newProfile)
.ReceiveJson<SonarrReleaseProfile>();
@ -65,7 +63,7 @@ namespace TrashLib.Sonarr.Api
public async Task<IReadOnlyCollection<SonarrQualityDefinitionItem>> GetQualityDefinition()
{
return await BaseUrl()
return await _baseUrl
.AppendPathSegment("qualitydefinition")
.GetJsonAsync<List<SonarrQualityDefinitionItem>>();
}
@ -73,12 +71,10 @@ namespace TrashLib.Sonarr.Api
public async Task<IList<SonarrQualityDefinitionItem>> UpdateQualityDefinition(
IReadOnlyCollection<SonarrQualityDefinitionItem> newQuality)
{
return await BaseUrl()
return await _baseUrl
.AppendPathSegment("qualityDefinition/update")
.PutJsonAsync(newQuality)
.ReceiveJson<List<SonarrQualityDefinitionItem>>();
}
private string BaseUrl() => _serverInfo.BuildUrl();
}
}

@ -9,7 +9,6 @@ namespace TrashLib.Sonarr.Config
{
public IList<ReleaseProfileConfig> ReleaseProfiles { get; set; } = new List<ReleaseProfileConfig>();
public SonarrQualityDefinitionType? QualityDefinition { get; init; }
public override string ServiceId => "sonarr";
}
public class ReleaseProfileConfig

Loading…
Cancel
Save