refactor: Remove DI for IServiceConfiguration in API services

pull/201/head
Robert Dailey 1 year ago
parent bcc65857df
commit e3d6d4f79a

@ -1,7 +1,6 @@
using System.IO.Abstractions;
using System.IO.Abstractions.Extensions;
using System.IO.Abstractions.TestingHelpers;
using System.Reactive.Linq;
using Autofac;
using Autofac.Features.ResolveAnything;
using NSubstitute;
@ -10,7 +9,6 @@ using Recyclarr.Common;
using Recyclarr.Common.TestLibrary;
using Recyclarr.TestLibrary;
using Recyclarr.TrashLib;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Repo.VersionControl;
using Recyclarr.TrashLib.Services.System;
using Recyclarr.TrashLib.Startup;
@ -43,12 +41,11 @@ public abstract class IntegrationFixture : IDisposable
builder.RegisterMockFor<IGitRepository>();
builder.RegisterMockFor<IGitRepositoryFactory>();
builder.RegisterMockFor<IServiceConfiguration>();
builder.RegisterMockFor<IEnvironment>();
builder.RegisterMockFor<IServiceInformation>(m =>
{
// By default, choose some extremely high number so that all the newest features are enabled.
m.Version.Returns(_ => Observable.Return(new Version("99.0.0.0")));
m.GetVersion(default!).ReturnsForAnyArgs(_ => new Version("99.0.0.0"));
});
RegisterExtraTypes(builder);

@ -1,49 +1,36 @@
using Autofac;
using FluentAssertions;
using NSubstitute;
using NUnit.Framework;
using Recyclarr.Cli.Console.Helpers;
using Recyclarr.Cli.TestLibrary;
using Recyclarr.TestLibrary.AutoFixture;
using Recyclarr.TrashLib.Config.Services;
namespace Recyclarr.Cli.Tests.Console.Helpers;
[TestFixture]
[Parallelizable(ParallelScope.All)]
public class CacheStoragePathTest : IntegrationFixture
public class CacheStoragePathTest
{
[Test]
public void Use_guid_when_no_name()
[Test, AutoMockData]
public void Use_guid_when_no_name(CacheStoragePath sut)
{
var config = Substitute.ForPartsOf<ServiceConfiguration>();
config.BaseUrl = new Uri("http://something");
config.InstanceName = null;
using var scope = Container.BeginLifetimeScope(builder =>
{
builder.RegisterInstance(config).AsImplementedInterfaces();
});
var sut = scope.Resolve<CacheStoragePath>();
var result = sut.CalculatePath("obj");
var result = sut.CalculatePath(config, "obj");
result.FullName.Should().MatchRegex(@".*[/\\][a-f0-9]+[/\\]obj\.json$");
}
[Test]
public void Use_name_when_not_null()
[Test, AutoMockData]
public void Use_name_when_not_null(CacheStoragePath sut)
{
var config = Substitute.ForPartsOf<ServiceConfiguration>();
config.BaseUrl = new Uri("http://something");
config.InstanceName = "thename";
using var scope = Container.BeginLifetimeScope(builder =>
{
builder.RegisterInstance(config).AsImplementedInterfaces();
});
var sut = scope.Resolve<CacheStoragePath>();
var result = sut.CalculatePath("obj");
var result = sut.CalculatePath(config, "obj");
result.FullName.Should().MatchRegex(@".*[/\\]thename_[a-f0-9]+[/\\]obj\.json$");
}

@ -11,39 +11,36 @@ namespace Recyclarr.Cli.Console.Helpers;
public class CacheStoragePath : ICacheStoragePath
{
private readonly IAppPaths _paths;
private readonly IServiceConfiguration _config;
private readonly IFNV1a _hash;
public CacheStoragePath(
IAppPaths paths,
IServiceConfiguration config)
IAppPaths paths)
{
_paths = paths;
_config = config;
_hash = FNV1aFactory.Instance.Create(FNVConfig.GetPredefinedConfig(32));
}
private string BuildUniqueServiceDir()
private string BuildUniqueServiceDir(IServiceConfiguration config)
{
// In the future, once array-style configurations are removed, the service name will no longer be optional
// and the below condition can be removed and the logic simplified.
var dirName = new StringBuilder();
if (_config.InstanceName is not null)
if (config.InstanceName is not null)
{
dirName.Append($"{_config.InstanceName}_");
dirName.Append($"{config.InstanceName}_");
}
var url = _config.BaseUrl.OriginalString;
var url = config.BaseUrl.OriginalString;
var guid = _hash.ComputeHash(Encoding.ASCII.GetBytes(url)).AsHexString();
dirName.Append(guid);
return dirName.ToString();
}
public IFileInfo CalculatePath(string cacheObjectName)
public IFileInfo CalculatePath(IServiceConfiguration config, string cacheObjectName)
{
return _paths.CacheDirectory
.SubDirectory(_config.ServiceType.ToString().ToLower(CultureInfo.CurrentCulture))
.SubDirectory(BuildUniqueServiceDir())
.SubDirectory(config.ServiceType.ToString().ToLower(CultureInfo.CurrentCulture))
.SubDirectory(BuildUniqueServiceDir(config))
.File(cacheObjectName + ".json");
}
}

@ -6,6 +6,7 @@ using NSubstitute;
using NUnit.Framework;
using Recyclarr.TestLibrary.AutoFixture;
using Recyclarr.TrashLib.Cache;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Services.CustomFormat.Models.Cache;
namespace Recyclarr.TrashLib.Tests.Cache;
@ -34,9 +35,10 @@ public class ServiceCacheTest
[Test, AutoMockData]
public void Load_returns_null_when_file_does_not_exist(
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
IServiceConfiguration config,
ServiceCache sut)
{
var result = sut.Load<ObjectWithAttribute>();
var result = sut.Load<ObjectWithAttribute>(config);
result.Should().BeNull();
}
@ -44,6 +46,7 @@ public class ServiceCacheTest
public void Loading_with_attribute_parses_correctly(
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
[Frozen] ICacheStoragePath storage,
IServiceConfiguration config,
ServiceCache sut)
{
const string testJson = @"{'test_value': 'Foo'}";
@ -51,18 +54,20 @@ public class ServiceCacheTest
const string testJsonPath = "cacheFile.json";
fs.AddFile(testJsonPath, new MockFileData(testJson));
storage.CalculatePath(default!).ReturnsForAnyArgs(fs.FileInfo.New(testJsonPath));
storage.CalculatePath(default!, default!).ReturnsForAnyArgs(fs.FileInfo.New(testJsonPath));
var obj = sut.Load<ObjectWithAttribute>();
var obj = sut.Load<ObjectWithAttribute>(config);
obj.Should().NotBeNull();
obj!.TestValue.Should().Be("Foo");
}
[Test, AutoMockData]
public void Loading_with_invalid_object_name_throws(ServiceCache sut)
public void Loading_with_invalid_object_name_throws(
IServiceConfiguration config,
ServiceCache sut)
{
Action act = () => sut.Load<ObjectWithAttributeInvalidChars>();
Action act = () => sut.Load<ObjectWithAttributeInvalidChars>(config);
act.Should()
.Throw<ArgumentException>()
@ -70,9 +75,11 @@ public class ServiceCacheTest
}
[Test, AutoMockData]
public void Loading_without_attribute_throws(ServiceCache sut)
public void Loading_without_attribute_throws(
IServiceConfiguration config,
ServiceCache sut)
{
Action act = () => sut.Load<ObjectWithoutAttribute>();
Action act = () => sut.Load<ObjectWithoutAttribute>(config);
act.Should()
.Throw<ArgumentException>()
@ -83,15 +90,17 @@ public class ServiceCacheTest
public void Properties_are_saved_using_snake_case(
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
[Frozen] ICacheStoragePath storage,
IServiceConfiguration config,
ServiceCache sut)
{
storage.CalculatePath(default!).ReturnsForAnyArgs(_ => fs.FileInfo.New($"{ValidObjectName}.json"));
storage.CalculatePath(default!, default!)
.ReturnsForAnyArgs(_ => fs.FileInfo.New($"{ValidObjectName}.json"));
sut.Save(new ObjectWithAttribute {TestValue = "Foo"});
sut.Save(new ObjectWithAttribute {TestValue = "Foo"}, config);
fs.AllFiles.Should().ContainMatch($"*{ValidObjectName}.json");
var file = fs.GetFile(storage.CalculatePath("").FullName);
var file = fs.GetFile(storage.CalculatePath(config, "").FullName);
file.Should().NotBeNull();
file.TextContents.Should().Contain("\"test_value\"");
}
@ -100,12 +109,13 @@ public class ServiceCacheTest
public void Saving_with_attribute_parses_correctly(
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
[Frozen] ICacheStoragePath storage,
IServiceConfiguration config,
ServiceCache sut)
{
const string testJsonPath = "cacheFile.json";
storage.CalculatePath(default!).ReturnsForAnyArgs(fs.FileInfo.New(testJsonPath));
storage.CalculatePath(default!, default!).ReturnsForAnyArgs(fs.FileInfo.New(testJsonPath));
sut.Save(new ObjectWithAttribute {TestValue = "Foo"});
sut.Save(new ObjectWithAttribute {TestValue = "Foo"}, config);
var expectedFile = fs.GetFile(testJsonPath);
expectedFile.Should().NotBeNull();
@ -115,9 +125,11 @@ public class ServiceCacheTest
}
[Test, AutoMockData]
public void Saving_with_invalid_object_name_throws(ServiceCache sut)
public void Saving_with_invalid_object_name_throws(
IServiceConfiguration config,
ServiceCache sut)
{
var act = () => sut.Save(new ObjectWithAttributeInvalidChars());
var act = () => sut.Save(new ObjectWithAttributeInvalidChars(), config);
act.Should()
.Throw<ArgumentException>()
@ -125,9 +137,11 @@ public class ServiceCacheTest
}
[Test, AutoMockData]
public void Saving_without_attribute_throws(ServiceCache sut)
public void Saving_without_attribute_throws(
IServiceConfiguration config,
ServiceCache sut)
{
var act = () => sut.Save(new ObjectWithoutAttribute());
var act = () => sut.Save(new ObjectWithoutAttribute(), config);
act.Should()
.Throw<ArgumentException>()
@ -138,13 +152,14 @@ public class ServiceCacheTest
public void Switching_config_and_base_url_should_yield_different_cache_paths(
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
[Frozen] ICacheStoragePath storage,
IServiceConfiguration config,
ServiceCache sut)
{
storage.CalculatePath(default!).ReturnsForAnyArgs(fs.FileInfo.New("Foo.json"));
sut.Save(new ObjectWithAttribute {TestValue = "Foo"});
storage.CalculatePath(default!, default!).ReturnsForAnyArgs(fs.FileInfo.New("Foo.json"));
sut.Save(new ObjectWithAttribute {TestValue = "Foo"}, config);
storage.CalculatePath(default!).ReturnsForAnyArgs(fs.FileInfo.New("Bar.json"));
sut.Save(new ObjectWithAttribute {TestValue = "Bar"});
storage.CalculatePath(default!, default!).ReturnsForAnyArgs(fs.FileInfo.New("Bar.json"));
sut.Save(new ObjectWithAttribute {TestValue = "Bar"}, config);
var expectedFiles = new[] {"*Foo.json", "*Bar.json"};
foreach (var expectedFile in expectedFiles)
@ -157,12 +172,13 @@ public class ServiceCacheTest
public void When_cache_file_is_empty_do_not_throw(
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
[Frozen] ICacheStoragePath storage,
IServiceConfiguration config,
ServiceCache sut)
{
storage.CalculatePath(default!).ReturnsForAnyArgs(fs.FileInfo.New("cacheFile.json"));
storage.CalculatePath(default!, default!).ReturnsForAnyArgs(fs.FileInfo.New("cacheFile.json"));
fs.AddFile("cacheFile.json", new MockFileData(""));
Action act = () => sut.Load<ObjectWithAttribute>();
Action act = () => sut.Load<ObjectWithAttribute>(config);
act.Should().NotThrow();
}
@ -171,6 +187,7 @@ public class ServiceCacheTest
public void Name_properties_are_set_on_load(
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
[Frozen] ICacheStoragePath storage,
IServiceConfiguration config,
ServiceCache sut)
{
const string cacheJson = @"
@ -187,9 +204,9 @@ public class ServiceCacheTest
";
fs.AddFile("cacheFile.json", new MockFileData(cacheJson));
storage.CalculatePath(default!).ReturnsForAnyArgs(fs.FileInfo.New("cacheFile.json"));
storage.CalculatePath(default!, default!).ReturnsForAnyArgs(fs.FileInfo.New("cacheFile.json"));
var result = sut.Load<CustomFormatCache>();
var result = sut.Load<CustomFormatCache>(config);
result.Should().BeEquivalentTo(new CustomFormatCache
{

@ -3,6 +3,7 @@ using FluentAssertions;
using NSubstitute;
using NUnit.Framework;
using Recyclarr.TrashLib.Cache;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Services.CustomFormat;
using Recyclarr.TrashLib.Services.CustomFormat.Models;
using Recyclarr.TrashLib.Services.CustomFormat.Models.Cache;
@ -33,14 +34,15 @@ public class CachePersisterTest
public void Set_loaded_cache_to_null_if_versions_mismatch(int versionToTest)
{
var ctx = new Context();
var config = Substitute.For<IServiceConfiguration>();
var testCfObj = new CustomFormatCache
{
Version = versionToTest,
TrashIdMappings = new Collection<TrashIdMapping> {new("", "", 5)}
};
ctx.ServiceCache.Load<CustomFormatCache>().Returns(testCfObj);
ctx.Persister.Load();
ctx.ServiceCache.Load<CustomFormatCache>(config).Returns(testCfObj);
ctx.Persister.Load(config);
ctx.Persister.CfCache.Should().BeNull();
}
@ -48,14 +50,15 @@ public class CachePersisterTest
public void Accept_loaded_cache_when_versions_match()
{
var ctx = new Context();
var config = Substitute.For<IServiceConfiguration>();
var testCfObj = new CustomFormatCache
{
Version = CustomFormatCache.LatestVersion,
TrashIdMappings = new Collection<TrashIdMapping> {new("", "", 5)}
};
ctx.ServiceCache.Load<CustomFormatCache>().Returns(testCfObj);
ctx.Persister.Load();
ctx.ServiceCache.Load<CustomFormatCache>(config).Returns(testCfObj);
ctx.Persister.Load(config);
ctx.Persister.CfCache.Should().NotBeNull();
}
@ -64,9 +67,10 @@ public class CachePersisterTest
{
var ctx = new Context();
var testCfObj = new CustomFormatCache();
ctx.ServiceCache.Load<CustomFormatCache>().Returns(testCfObj);
var config = Substitute.For<IServiceConfiguration>();
ctx.Persister.Load();
ctx.ServiceCache.Load<CustomFormatCache>(config).Returns(testCfObj);
ctx.Persister.Load(config);
ctx.Persister.CfCache.Should().BeSameAs(testCfObj);
}
@ -74,7 +78,9 @@ public class CachePersisterTest
public void Cf_cache_returns_null_if_not_loaded()
{
var ctx = new Context();
ctx.Persister.Load();
var config = Substitute.For<IServiceConfiguration>();
ctx.Persister.Load(config);
ctx.Persister.CfCache.Should().BeNull();
}
@ -83,33 +89,39 @@ public class CachePersisterTest
{
var ctx = new Context();
var testCfObj = new CustomFormatCache();
ctx.ServiceCache.Load<CustomFormatCache>().Returns(testCfObj);
var config = Substitute.For<IServiceConfiguration>();
ctx.ServiceCache.Load<CustomFormatCache>(config).Returns(testCfObj);
ctx.Persister.Load();
ctx.Persister.Save();
ctx.Persister.Load(config);
ctx.Persister.Save(config);
ctx.ServiceCache.Received().Save(Arg.Is(testCfObj));
ctx.ServiceCache.Received().Save(testCfObj, config);
}
[Test]
public void Saving_without_loading_does_nothing()
{
var ctx = new Context();
ctx.Persister.Save();
ctx.ServiceCache.DidNotReceive().Save(Arg.Any<object>());
var config = Substitute.For<IServiceConfiguration>();
ctx.Persister.Save(config);
ctx.ServiceCache.DidNotReceiveWithAnyArgs().Save(Arg.Any<object>(), default!);
}
[Test]
public void Updating_overwrites_previous_cf_cache_and_updates_cf_data()
{
var ctx = new Context();
var config = Substitute.For<IServiceConfiguration>();
// Load initial CfCache just to test that it gets replaced
ctx.ServiceCache.Load<CustomFormatCache>().Returns(new CustomFormatCache
ctx.ServiceCache.Load<CustomFormatCache>(config).Returns(new CustomFormatCache
{
TrashIdMappings = new Collection<TrashIdMapping> {new("trashid", "", 1)}
});
ctx.Persister.Load();
ctx.Persister.Load(config);
// Update with new cached items
var customFormatData = new List<ProcessedCustomFormatData>

@ -19,7 +19,6 @@ public class PersistenceProcessorTest
{
var steps = Substitute.For<IPersistenceProcessorSteps>();
var cfApi = Substitute.For<ICustomFormatService>();
var qpApi = Substitute.For<IQualityProfileService>();
var config = new RadarrConfiguration {DeleteOldCustomFormats = true};
@ -27,8 +26,8 @@ public class PersistenceProcessorTest
var deletedCfsInCache = new Collection<TrashIdMapping>();
var profileScores = new Dictionary<string, QualityProfileCustomFormatScoreMapping>();
var processor = new PersistenceProcessor(cfApi, qpApi, config, steps);
await processor.PersistCustomFormats(guideCfs, deletedCfsInCache, profileScores);
var processor = new PersistenceProcessor(cfApi, steps);
await processor.PersistCustomFormats(config, guideCfs, deletedCfsInCache, profileScores);
steps.JsonTransactionStep.Received().RecordDeletions(Arg.Is(deletedCfsInCache), Arg.Any<List<JObject>>());
}
@ -38,7 +37,6 @@ public class PersistenceProcessorTest
{
var steps = Substitute.For<IPersistenceProcessorSteps>();
var cfApi = Substitute.For<ICustomFormatService>();
var qpApi = Substitute.For<IQualityProfileService>();
var config = new RadarrConfiguration {DeleteOldCustomFormats = false};
@ -46,8 +44,8 @@ public class PersistenceProcessorTest
var deletedCfsInCache = Array.Empty<TrashIdMapping>();
var profileScores = new Dictionary<string, QualityProfileCustomFormatScoreMapping>();
var processor = new PersistenceProcessor(cfApi, qpApi, config, steps);
await processor.PersistCustomFormats(guideCfs, deletedCfsInCache, profileScores);
var processor = new PersistenceProcessor(cfApi, steps);
await processor.PersistCustomFormats(config, guideCfs, deletedCfsInCache, profileScores);
steps.JsonTransactionStep.DidNotReceive()
.RecordDeletions(Arg.Any<IEnumerable<TrashIdMapping>>(), Arg.Any<List<JObject>>());

@ -1,5 +1,6 @@
using NSubstitute;
using NUnit.Framework;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Services.CustomFormat.Api;
using Recyclarr.TrashLib.Services.CustomFormat.Models;
using Recyclarr.TrashLib.Services.CustomFormat.Models.Cache;
@ -28,14 +29,15 @@ public class CustomFormatApiPersistenceStepTest
var api = Substitute.For<ICustomFormatService>();
var processor = new CustomFormatApiPersistenceStep();
await processor.Process(api, transactions);
var processor = new CustomFormatApiPersistenceStep(api);
var config = Substitute.For<IServiceConfiguration>();
await processor.Process(config, transactions);
Received.InOrder(() =>
{
api.CreateCustomFormat(transactions.NewCustomFormats.First());
api.UpdateCustomFormat(transactions.UpdatedCustomFormats.First());
api.DeleteCustomFormat(4);
api.CreateCustomFormat(config, transactions.NewCustomFormats.First());
api.UpdateCustomFormat(config, transactions.UpdatedCustomFormats.First());
api.DeleteCustomFormat(config, 4);
});
}
}

@ -5,6 +5,7 @@ using Newtonsoft.Json.Linq;
using NSubstitute;
using NUnit.Framework;
using Recyclarr.TestLibrary.NSubstitute;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Services.CustomFormat.Api;
using Recyclarr.TrashLib.Services.CustomFormat.Models;
using Recyclarr.TrashLib.Services.CustomFormat.Processors.PersistenceSteps;
@ -41,7 +42,8 @@ public class QualityProfileApiPersistenceStepTest
}]";
var api = Substitute.For<IQualityProfileService>();
api.GetQualityProfiles()!.Returns(JsonConvert.DeserializeObject<List<JObject>>(radarrQualityProfileData));
api.GetQualityProfiles(default!)!.ReturnsForAnyArgs(
JsonConvert.DeserializeObject<List<JObject>>(radarrQualityProfileData));
var cfScores = new Dictionary<string, QualityProfileCustomFormatScoreMapping>
{
@ -50,10 +52,10 @@ public class QualityProfileApiPersistenceStepTest
}
};
var processor = new QualityProfileApiPersistenceStep();
await processor.Process(api, cfScores);
var processor = new QualityProfileApiPersistenceStep(api);
await processor.Process(Substitute.For<IServiceConfiguration>(), cfScores);
await api.DidNotReceive().UpdateQualityProfile(Arg.Any<JObject>(), Arg.Any<int>());
await api.DidNotReceiveWithAnyArgs().UpdateQualityProfile(default!, default!, default!);
}
[Test]
@ -62,15 +64,16 @@ public class QualityProfileApiPersistenceStepTest
const string radarrQualityProfileData = @"[{'name': 'profile1'}]";
var api = Substitute.For<IQualityProfileService>();
api.GetQualityProfiles()!.Returns(JsonConvert.DeserializeObject<List<JObject>>(radarrQualityProfileData));
api.GetQualityProfiles(default!)!.ReturnsForAnyArgs(
JsonConvert.DeserializeObject<List<JObject>>(radarrQualityProfileData));
var cfScores = new Dictionary<string, QualityProfileCustomFormatScoreMapping>
{
{"wrong_profile_name", CfTestUtils.NewMapping()}
};
var processor = new QualityProfileApiPersistenceStep();
await processor.Process(api, cfScores);
var processor = new QualityProfileApiPersistenceStep(api);
await processor.Process(Substitute.For<IServiceConfiguration>(), cfScores);
processor.InvalidProfileNames.Should().Equal("wrong_profile_name");
processor.UpdatedScores.Should().BeEmpty();
@ -101,7 +104,8 @@ public class QualityProfileApiPersistenceStepTest
}]";
var api = Substitute.For<IQualityProfileService>();
api.GetQualityProfiles()!.Returns(JsonConvert.DeserializeObject<List<JObject>>(radarrQualityProfileData));
api.GetQualityProfiles(default!)!.ReturnsForAnyArgs(
JsonConvert.DeserializeObject<List<JObject>>(radarrQualityProfileData));
var cfScores = new Dictionary<string, QualityProfileCustomFormatScoreMapping>
{
@ -111,8 +115,8 @@ public class QualityProfileApiPersistenceStepTest
}
};
var processor = new QualityProfileApiPersistenceStep();
await processor.Process(api, cfScores);
var processor = new QualityProfileApiPersistenceStep(api);
await processor.Process(Substitute.For<IServiceConfiguration>(), cfScores);
processor.InvalidProfileNames.Should().BeEmpty();
processor.UpdatedScores.Should()
@ -125,6 +129,7 @@ public class QualityProfileApiPersistenceStepTest
});
await api.Received().UpdateQualityProfile(
Arg.Any<IServiceConfiguration>(),
Verify.That<JObject>(j => j["formatItems"]!.Children().Should().HaveCount(3)),
Arg.Any<int>());
}
@ -174,7 +179,8 @@ public class QualityProfileApiPersistenceStepTest
}]";
var api = Substitute.For<IQualityProfileService>();
api.GetQualityProfiles()!.Returns(JsonConvert.DeserializeObject<List<JObject>>(radarrQualityProfileData));
api.GetQualityProfiles(default!)!.ReturnsForAnyArgs(
JsonConvert.DeserializeObject<List<JObject>>(radarrQualityProfileData));
var cfScores = new Dictionary<string, QualityProfileCustomFormatScoreMapping>
{
@ -189,8 +195,9 @@ public class QualityProfileApiPersistenceStepTest
}
};
var processor = new QualityProfileApiPersistenceStep();
await processor.Process(api, cfScores);
var processor = new QualityProfileApiPersistenceStep(api);
var config = Substitute.For<IServiceConfiguration>();
await processor.Process(config, cfScores);
var expectedProfileJson = JObject.Parse(@"{
'name': 'profile1',
@ -234,7 +241,10 @@ public class QualityProfileApiPersistenceStepTest
}");
await api.Received()
.UpdateQualityProfile(Verify.That<JObject>(a => a.Should().BeEquivalentTo(expectedProfileJson)), 1);
.UpdateQualityProfile(
config,
Verify.That<JObject>(a => a.Should().BeEquivalentTo(expectedProfileJson)),
1);
processor.InvalidProfileNames.Should().BeEmpty();
processor.UpdatedScores.Should()
.ContainKey("profile1").WhoseValue.Should()

@ -21,11 +21,11 @@ public class SonarrCapabilityEnforcerTest
{
var config = new SonarrConfiguration();
checker.GetCapabilities().Returns((SonarrCapabilities?) null);
checker.GetCapabilities(default!).ReturnsForAnyArgs((SonarrCapabilities?) null);
var act = () => sut.Check(config);
act.Should().Throw<ServiceIncompatibilityException>().WithMessage("*obtained*");
act.Should().ThrowAsync<ServiceIncompatibilityException>().WithMessage("*obtained*");
}
[Test, AutoMockData]
@ -35,14 +35,14 @@ public class SonarrCapabilityEnforcerTest
{
var config = new SonarrConfiguration();
checker.GetCapabilities().Returns(new SonarrCapabilities(new Version())
checker.GetCapabilities(default!).ReturnsForAnyArgs(new SonarrCapabilities(new Version())
{
SupportsNamedReleaseProfiles = false
});
var act = () => sut.Check(config);
act.Should().Throw<ServiceIncompatibilityException>().WithMessage("*minimum*");
act.Should().ThrowAsync<ServiceIncompatibilityException>().WithMessage("*minimum*");
}
[Test, AutoMockData]
@ -58,7 +58,7 @@ public class SonarrCapabilityEnforcerTest
}
};
checker.GetCapabilities().Returns(new SonarrCapabilities(new Version())
checker.GetCapabilities(default!).ReturnsForAnyArgs(new SonarrCapabilities(new Version())
{
SupportsNamedReleaseProfiles = true,
SupportsCustomFormats = true
@ -66,7 +66,7 @@ public class SonarrCapabilityEnforcerTest
var act = () => sut.Check(config);
act.Should().Throw<ServiceIncompatibilityException>().WithMessage("*v3*");
act.Should().ThrowAsync<ServiceIncompatibilityException>().WithMessage("*v3*");
}
[Test, AutoMockData]
@ -82,7 +82,7 @@ public class SonarrCapabilityEnforcerTest
}
};
checker.GetCapabilities().Returns(new SonarrCapabilities(new Version())
checker.GetCapabilities(default!).ReturnsForAnyArgs(new SonarrCapabilities(new Version())
{
SupportsNamedReleaseProfiles = true,
SupportsCustomFormats = false
@ -90,6 +90,6 @@ public class SonarrCapabilityEnforcerTest
var act = () => sut.Check(config);
act.Should().Throw<ServiceIncompatibilityException>().WithMessage("*v4*");
act.Should().ThrowAsync<ServiceIncompatibilityException>().WithMessage("*v4*");
}
}

@ -7,6 +7,7 @@ using NSubstitute;
using NUnit.Framework;
using Recyclarr.Cli.TestLibrary;
using Recyclarr.TestLibrary;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Services.ReleaseProfile.Api;
using Recyclarr.TrashLib.Services.ReleaseProfile.Api.Objects;
using Recyclarr.TrashLib.Services.Sonarr.Capabilities;
@ -58,10 +59,10 @@ public class SonarrCompatibilityTest : IntegrationFixture
}
[Test]
public void Send_v2_to_v1()
public async Task Send_v2_to_v1()
{
var capabilityChecker = Resolve<ISonarrCapabilityChecker>();
capabilityChecker.GetCapabilities().Returns(new SonarrCapabilities(new Version())
capabilityChecker.GetCapabilities(default!).ReturnsForAnyArgs(new SonarrCapabilities(new Version())
{
ArraysNeededForReleaseProfileRequiredAndIgnored = false
});
@ -69,16 +70,16 @@ public class SonarrCompatibilityTest : IntegrationFixture
var sut = Resolve<SonarrReleaseProfileCompatibilityHandler>();
var data = new SonarrReleaseProfile {Ignored = new List<string> {"one", "two", "three"}};
var result = sut.CompatibleReleaseProfileForSending(data);
var result = await sut.CompatibleReleaseProfileForSending(Substitute.For<IServiceConfiguration>(), data);
result.Should().BeEquivalentTo(new SonarrReleaseProfileV1 {Ignored = "one,two,three"});
}
[Test]
public void Send_v2_to_v2()
public async Task Send_v2_to_v2()
{
var capabilityChecker = Resolve<ISonarrCapabilityChecker>();
capabilityChecker.GetCapabilities().Returns(new SonarrCapabilities(new Version())
capabilityChecker.GetCapabilities(default!).ReturnsForAnyArgs(new SonarrCapabilities(new Version())
{
ArraysNeededForReleaseProfileRequiredAndIgnored = true
});
@ -86,7 +87,7 @@ public class SonarrCompatibilityTest : IntegrationFixture
var sut = Resolve<SonarrReleaseProfileCompatibilityHandler>();
var data = new SonarrReleaseProfile {Ignored = new List<string> {"one", "two", "three"}};
var result = sut.CompatibleReleaseProfileForSending(data);
var result = await sut.CompatibleReleaseProfileForSending(Substitute.For<IServiceConfiguration>(), data);
result.Should().BeEquivalentTo(data);
}

@ -1,8 +1,9 @@
using System.IO.Abstractions;
using Recyclarr.TrashLib.Config.Services;
namespace Recyclarr.TrashLib.Cache;
public interface ICacheStoragePath
{
IFileInfo CalculatePath(string cacheObjectName);
IFileInfo CalculatePath(IServiceConfiguration config, string cacheObjectName);
}

@ -1,7 +1,9 @@
using Recyclarr.TrashLib.Config.Services;
namespace Recyclarr.TrashLib.Cache;
public interface IServiceCache
{
T? Load<T>() where T : class;
void Save<T>(T obj) where T : class;
T? Load<T>(IServiceConfiguration config) where T : class;
void Save<T>(T obj, IServiceConfiguration config) where T : class;
}

@ -4,6 +4,7 @@ using System.Text.RegularExpressions;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Recyclarr.Common.Extensions;
using Recyclarr.TrashLib.Config.Services;
namespace Recyclarr.TrashLib.Cache;
@ -29,9 +30,9 @@ public class ServiceCache : IServiceCache
private ILogger Log { get; }
public T? Load<T>() where T : class
public T? Load<T>(IServiceConfiguration config) where T : class
{
var path = PathFromAttribute<T>();
var path = PathFromAttribute<T>(config);
if (!path.Exists)
{
return null;
@ -52,9 +53,9 @@ public class ServiceCache : IServiceCache
return null;
}
public void Save<T>(T obj) where T : class
public void Save<T>(T obj, IServiceConfiguration config) where T : class
{
var path = PathFromAttribute<T>();
var path = PathFromAttribute<T>(config);
path.CreateParentDirectory();
var serializer = JsonSerializer.Create(_jsonSettings);
@ -74,7 +75,7 @@ public class ServiceCache : IServiceCache
return attribute.Name;
}
private IFileInfo PathFromAttribute<T>()
private IFileInfo PathFromAttribute<T>(IServiceConfiguration config)
{
var objectName = GetCacheObjectNameAttribute<T>();
if (!AllowedObjectNameCharacters.IsMatch(objectName))
@ -82,6 +83,6 @@ public class ServiceCache : IServiceCache
throw new ArgumentException($"Object name '{objectName}' has unacceptable characters");
}
return _storagePath.CalculatePath(objectName);
return _storagePath.CalculatePath(config, objectName);
}
}

@ -7,4 +7,6 @@ public interface IServiceConfiguration
Uri BaseUrl { get; }
string ApiKey { get; }
bool DeleteOldCustomFormats { get; }
ICollection<CustomFormatConfig> CustomFormats { get; init; }
QualityDefinitionConfig? QualityDefinition { get; init; }
}

@ -1,8 +1,9 @@
using Flurl.Http;
using Recyclarr.TrashLib.Config.Services;
namespace Recyclarr.TrashLib.Http;
public interface IServiceRequestBuilder
{
IFlurlRequest Request(params object[] path);
IFlurlRequest Request(IServiceConfiguration config, params object[] path);
}

@ -5,19 +5,17 @@ namespace Recyclarr.TrashLib.Http;
public class ServiceRequestBuilder : IServiceRequestBuilder
{
private readonly IServiceConfiguration _config;
private readonly IFlurlClientFactory _clientFactory;
public ServiceRequestBuilder(IServiceConfiguration config, IFlurlClientFactory clientFactory)
public ServiceRequestBuilder(IFlurlClientFactory clientFactory)
{
_config = config;
_clientFactory = clientFactory;
}
public IFlurlRequest Request(params object[] path)
public IFlurlRequest Request(IServiceConfiguration config, params object[] path)
{
var client = _clientFactory.BuildClient(_config.BaseUrl);
var client = _clientFactory.BuildClient(config.BaseUrl);
return client.Request(new[] {"api", "v3"}.Concat(path).ToArray())
.SetQueryParams(new {apikey = _config.ApiKey});
.SetQueryParams(new {apikey = config.ApiKey});
}
}

@ -1,24 +1,21 @@
using System.Reactive.Linq;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Services.System;
namespace Recyclarr.TrashLib.Services.Common;
public abstract class ServiceCapabilityChecker<T> where T : class
{
private readonly IObservable<T?> _capabilities;
private readonly IServiceInformation _info;
public T? GetCapabilities()
protected ServiceCapabilityChecker(IServiceInformation info)
{
return _capabilities.Wait();
_info = info;
}
protected ServiceCapabilityChecker(IServiceInformation info)
public async Task<T?> GetCapabilities(IServiceConfiguration config)
{
_capabilities = info.Version
.Select(x => x is null ? null : BuildCapabilitiesObject(x))
.Replay(1)
.AutoConnect()
.LastAsync();
var version = await _info.GetVersion(config);
return version is not null ? BuildCapabilitiesObject(version) : null;
}
protected abstract T BuildCapabilitiesObject(Version version);

@ -1,5 +1,6 @@
using Flurl.Http;
using Newtonsoft.Json.Linq;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Http;
using Recyclarr.TrashLib.Services.CustomFormat.Models;
@ -14,15 +15,15 @@ internal class CustomFormatService : ICustomFormatService
_service = service;
}
public async Task<List<JObject>> GetCustomFormats()
public async Task<List<JObject>> GetCustomFormats(IServiceConfiguration config)
{
return await _service.Request("customformat")
return await _service.Request(config, "customformat")
.GetJsonAsync<List<JObject>>();
}
public async Task CreateCustomFormat(ProcessedCustomFormatData cf)
public async Task CreateCustomFormat(IServiceConfiguration config, ProcessedCustomFormatData cf)
{
var response = await _service.Request("customformat")
var response = await _service.Request(config, "customformat")
.PostJsonAsync(cf.Json)
.ReceiveJson<JObject>();
@ -32,16 +33,16 @@ internal class CustomFormatService : ICustomFormatService
}
}
public async Task UpdateCustomFormat(ProcessedCustomFormatData cf)
public async Task UpdateCustomFormat(IServiceConfiguration config, ProcessedCustomFormatData cf)
{
await _service.Request("customformat", cf.FormatId)
await _service.Request(config, "customformat", cf.FormatId)
.PutJsonAsync(cf.Json)
.ReceiveJson<JObject>();
}
public async Task DeleteCustomFormat(int customFormatId)
public async Task DeleteCustomFormat(IServiceConfiguration config, int customFormatId)
{
await _service.Request("customformat", customFormatId)
await _service.Request(config, "customformat", customFormatId)
.DeleteAsync();
}
}

@ -1,12 +1,13 @@
using Newtonsoft.Json.Linq;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Services.CustomFormat.Models;
namespace Recyclarr.TrashLib.Services.CustomFormat.Api;
public interface ICustomFormatService
{
Task<List<JObject>> GetCustomFormats();
Task CreateCustomFormat(ProcessedCustomFormatData cf);
Task UpdateCustomFormat(ProcessedCustomFormatData cf);
Task DeleteCustomFormat(int customFormatId);
Task<List<JObject>> GetCustomFormats(IServiceConfiguration config);
Task CreateCustomFormat(IServiceConfiguration config, ProcessedCustomFormatData cf);
Task UpdateCustomFormat(IServiceConfiguration config, ProcessedCustomFormatData cf);
Task DeleteCustomFormat(IServiceConfiguration config, int customFormatId);
}

@ -1,9 +1,10 @@
using Newtonsoft.Json.Linq;
using Recyclarr.TrashLib.Config.Services;
namespace Recyclarr.TrashLib.Services.CustomFormat.Api;
public interface IQualityProfileService
{
Task<List<JObject>> GetQualityProfiles();
Task<JObject> UpdateQualityProfile(JObject profileJson, int id);
Task<List<JObject>> GetQualityProfiles(IServiceConfiguration config);
Task<JObject> UpdateQualityProfile(IServiceConfiguration config, JObject profileJson, int id);
}

@ -1,5 +1,6 @@
using Flurl.Http;
using Newtonsoft.Json.Linq;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Http;
namespace Recyclarr.TrashLib.Services.CustomFormat.Api;
@ -13,15 +14,15 @@ internal class QualityProfileService : IQualityProfileService
_service = service;
}
public async Task<List<JObject>> GetQualityProfiles()
public async Task<List<JObject>> GetQualityProfiles(IServiceConfiguration config)
{
return await _service.Request("qualityprofile")
return await _service.Request(config, "qualityprofile")
.GetJsonAsync<List<JObject>>();
}
public async Task<JObject> UpdateQualityProfile(JObject profileJson, int id)
public async Task<JObject> UpdateQualityProfile(IServiceConfiguration config, JObject profileJson, int id)
{
return await _service.Request("qualityprofile", id)
return await _service.Request(config, "qualityprofile", id)
.PutJsonAsync(profileJson)
.ReceiveJson<JObject>();
}

@ -1,5 +1,6 @@
using Recyclarr.Common.Extensions;
using Recyclarr.TrashLib.Cache;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Services.CustomFormat.Models;
using Recyclarr.TrashLib.Services.CustomFormat.Models.Cache;
@ -18,9 +19,9 @@ public class CachePersister : ICachePersister
private ILogger Log { get; }
public CustomFormatCache? CfCache { get; private set; }
public void Load()
public void Load(IServiceConfiguration config)
{
CfCache = _cache.Load<CustomFormatCache>();
CfCache = _cache.Load<CustomFormatCache>(config);
// ReSharper disable once ConvertIfStatementToConditionalTernaryExpression
if (CfCache != null)
{
@ -41,7 +42,7 @@ public class CachePersister : ICachePersister
}
}
public void Save()
public void Save(IServiceConfiguration config)
{
if (CfCache == null)
{
@ -50,7 +51,7 @@ public class CachePersister : ICachePersister
}
Log.Debug("Saving Cache");
_cache.Save(CfCache);
_cache.Save(CfCache, config);
}
public void Update(IEnumerable<ProcessedCustomFormatData> customFormats)

@ -1,5 +1,4 @@
using Recyclarr.Common.Extensions;
using Recyclarr.TrashLib.Config;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Services.CustomFormat.Processors;
using Recyclarr.TrashLib.Services.CustomFormat.Processors.PersistenceSteps;
@ -29,11 +28,11 @@ internal class CustomFormatUpdater : ICustomFormatUpdater
_console = console;
}
public async Task Process(bool isPreview, IEnumerable<CustomFormatConfig> configs, SupportedServices serviceType)
public async Task Process(bool isPreview, IServiceConfiguration config)
{
_cache.Load();
_cache.Load(config);
await _guideProcessor.BuildGuideDataAsync(configs, _cache.CfCache, serviceType);
await _guideProcessor.BuildGuideDataAsync(config.CustomFormats, _cache.CfCache, config.ServiceType);
if (!ValidateGuideDataAndCheckShouldProceed())
{
@ -48,6 +47,7 @@ internal class CustomFormatUpdater : ICustomFormatUpdater
}
await _persistenceProcessor.PersistCustomFormats(
config,
_guideProcessor.ProcessedCustomFormats,
_guideProcessor.DeletedCustomFormatsInCache,
_guideProcessor.ProfileScores);
@ -57,7 +57,7 @@ internal class CustomFormatUpdater : ICustomFormatUpdater
// Cache all the custom formats (using ID from API response).
_cache.Update(_guideProcessor.ProcessedCustomFormats);
_cache.Save();
_cache.Save(config);
}
private void PrintQualityProfileUpdates()

@ -1,3 +1,4 @@
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Services.CustomFormat.Models;
using Recyclarr.TrashLib.Services.CustomFormat.Models.Cache;
@ -6,7 +7,7 @@ namespace Recyclarr.TrashLib.Services.CustomFormat;
public interface ICachePersister
{
CustomFormatCache? CfCache { get; }
void Load();
void Save();
void Load(IServiceConfiguration config);
void Save(IServiceConfiguration config);
void Update(IEnumerable<ProcessedCustomFormatData> customFormats);
}

@ -1,9 +1,8 @@
using Recyclarr.TrashLib.Config;
using Recyclarr.TrashLib.Config.Services;
namespace Recyclarr.TrashLib.Services.CustomFormat;
public interface ICustomFormatUpdater
{
Task Process(bool isPreview, IEnumerable<CustomFormatConfig> configs, SupportedServices serviceType);
Task Process(bool isPreview, IServiceConfiguration config);
}

@ -1,3 +1,4 @@
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Services.CustomFormat.Models;
using Recyclarr.TrashLib.Services.CustomFormat.Models.Cache;
using Recyclarr.TrashLib.Services.CustomFormat.Processors.PersistenceSteps;
@ -10,7 +11,9 @@ public interface IPersistenceProcessor
IReadOnlyCollection<string> InvalidProfileNames { get; }
CustomFormatTransactionData Transactions { get; }
Task PersistCustomFormats(IReadOnlyCollection<ProcessedCustomFormatData> guideCfs,
Task PersistCustomFormats(
IServiceConfiguration config,
IReadOnlyCollection<ProcessedCustomFormatData> guideCfs,
IEnumerable<TrashIdMapping> deletedCfsInCache,
IDictionary<string, QualityProfileCustomFormatScoreMapping> profileScores);
}

@ -15,20 +15,14 @@ public interface IPersistenceProcessorSteps
internal class PersistenceProcessor : IPersistenceProcessor
{
private readonly IServiceConfiguration _config;
private readonly ICustomFormatService _customFormatService;
private readonly IQualityProfileService _qualityProfileService;
private readonly IPersistenceProcessorSteps _steps;
public PersistenceProcessor(
ICustomFormatService customFormatService,
IQualityProfileService qualityProfileService,
IServiceConfiguration config,
IPersistenceProcessorSteps steps)
{
_customFormatService = customFormatService;
_qualityProfileService = qualityProfileService;
_config = config;
_steps = steps;
}
@ -42,11 +36,12 @@ internal class PersistenceProcessor : IPersistenceProcessor
=> _steps.ProfileQualityProfileApiPersister.InvalidProfileNames;
public async Task PersistCustomFormats(
IServiceConfiguration config,
IReadOnlyCollection<ProcessedCustomFormatData> guideCfs,
IEnumerable<TrashIdMapping> deletedCfsInCache,
IDictionary<string, QualityProfileCustomFormatScoreMapping> profileScores)
{
var serviceCfs = await _customFormatService.GetCustomFormats();
var serviceCfs = await _customFormatService.GetCustomFormats(config);
// 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
@ -54,17 +49,16 @@ internal class PersistenceProcessor : IPersistenceProcessor
_steps.JsonTransactionStep.Process(guideCfs, serviceCfs);
// Step 1.1: Optionally record deletions of custom formats in cache but not in the guide
if (_config.DeleteOldCustomFormats)
if (config.DeleteOldCustomFormats)
{
_steps.JsonTransactionStep.RecordDeletions(deletedCfsInCache, serviceCfs);
}
// 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(config, _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(config, profileScores);
}
}

@ -1,24 +1,32 @@
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Services.CustomFormat.Api;
namespace Recyclarr.TrashLib.Services.CustomFormat.Processors.PersistenceSteps;
internal class CustomFormatApiPersistenceStep : ICustomFormatApiPersistenceStep
{
public async Task Process(ICustomFormatService api, CustomFormatTransactionData transactions)
private readonly ICustomFormatService _api;
public CustomFormatApiPersistenceStep(ICustomFormatService api)
{
_api = api;
}
public async Task Process(IServiceConfiguration config, CustomFormatTransactionData transactions)
{
foreach (var cf in transactions.NewCustomFormats)
{
await api.CreateCustomFormat(cf);
await _api.CreateCustomFormat(config, cf);
}
foreach (var cf in transactions.UpdatedCustomFormats)
{
await api.UpdateCustomFormat(cf);
await _api.UpdateCustomFormat(config, cf);
}
foreach (var cfId in transactions.DeletedCustomFormatIds)
{
await api.DeleteCustomFormat(cfId.CustomFormatId);
await _api.DeleteCustomFormat(config, cfId.CustomFormatId);
}
}
}

@ -1,8 +1,8 @@
using Recyclarr.TrashLib.Services.CustomFormat.Api;
using Recyclarr.TrashLib.Config.Services;
namespace Recyclarr.TrashLib.Services.CustomFormat.Processors.PersistenceSteps;
public interface ICustomFormatApiPersistenceStep
{
Task Process(ICustomFormatService api, CustomFormatTransactionData transactions);
Task Process(IServiceConfiguration config, CustomFormatTransactionData transactions);
}

@ -1,4 +1,4 @@
using Recyclarr.TrashLib.Services.CustomFormat.Api;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Services.CustomFormat.Models;
namespace Recyclarr.TrashLib.Services.CustomFormat.Processors.PersistenceSteps;
@ -8,6 +8,7 @@ public interface IQualityProfileApiPersistenceStep
IDictionary<string, List<UpdatedFormatScore>> UpdatedScores { get; }
IReadOnlyCollection<string> InvalidProfileNames { get; }
Task Process(IQualityProfileService api,
Task Process(
IServiceConfiguration config,
IDictionary<string, QualityProfileCustomFormatScoreMapping> cfScores);
}

@ -1,5 +1,6 @@
using Newtonsoft.Json.Linq;
using Recyclarr.Common.Extensions;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Services.CustomFormat.Api;
using Recyclarr.TrashLib.Services.CustomFormat.Models;
@ -7,16 +8,23 @@ namespace Recyclarr.TrashLib.Services.CustomFormat.Processors.PersistenceSteps;
internal class QualityProfileApiPersistenceStep : IQualityProfileApiPersistenceStep
{
private readonly IQualityProfileService _api;
private readonly List<string> _invalidProfileNames = new();
private readonly Dictionary<string, List<UpdatedFormatScore>> _updatedScores = new();
public IDictionary<string, List<UpdatedFormatScore>> UpdatedScores => _updatedScores;
public IReadOnlyCollection<string> InvalidProfileNames => _invalidProfileNames;
public async Task Process(IQualityProfileService api,
public QualityProfileApiPersistenceStep(IQualityProfileService api)
{
_api = api;
}
public async Task Process(
IServiceConfiguration config,
IDictionary<string, QualityProfileCustomFormatScoreMapping> cfScores)
{
var serviceProfiles = await api.GetQualityProfiles();
var serviceProfiles = await _api.GetQualityProfiles(config);
// Match quality profiles in Radarr to ones the user put in their config.
// For each match, we return a tuple including the list of custom format scores ("formatItems").
@ -71,7 +79,7 @@ internal class QualityProfileApiPersistenceStep : IQualityProfileApiPersistenceS
}
var jsonRoot = (JObject) formatItems.First().Root;
await api.UpdateQualityProfile(jsonRoot, jsonRoot.Value<int>("id"));
await _api.UpdateQualityProfile(config, jsonRoot, jsonRoot.Value<int>("id"));
}
}

@ -1,4 +1,3 @@
using Recyclarr.TrashLib.Config;
using Recyclarr.TrashLib.Services.CustomFormat;
using Recyclarr.TrashLib.Services.QualitySize;
using Recyclarr.TrashLib.Services.Radarr.Config;
@ -7,18 +6,15 @@ namespace Recyclarr.TrashLib.Services.Processors;
public class RadarrProcessor : IServiceProcessor
{
private readonly ILogger _log;
private readonly ICustomFormatUpdater _cfUpdater;
private readonly IQualitySizeUpdater _qualityUpdater;
private readonly RadarrConfiguration _config;
public RadarrProcessor(
ILogger log,
ICustomFormatUpdater cfUpdater,
IQualitySizeUpdater qualityUpdater,
RadarrConfiguration config)
{
_log = log;
_cfUpdater = cfUpdater;
_qualityUpdater = qualityUpdater;
_config = config;
@ -26,23 +22,14 @@ public class RadarrProcessor : IServiceProcessor
public async Task Process(ISyncSettings settings)
{
var didWork = false;
if (_config.QualityDefinition != null)
{
await _qualityUpdater.Process(settings.Preview, _config.QualityDefinition, SupportedServices.Radarr);
didWork = true;
await _qualityUpdater.Process(settings.Preview, _config);
}
if (_config.CustomFormats.Count > 0)
{
await _cfUpdater.Process(settings.Preview, _config.CustomFormats, SupportedServices.Radarr);
didWork = true;
}
if (!didWork)
{
_log.Information("Nothing to do");
await _cfUpdater.Process(settings.Preview, _config);
}
}
}

@ -1,4 +1,3 @@
using Recyclarr.TrashLib.Config;
using Recyclarr.TrashLib.Services.CustomFormat;
using Recyclarr.TrashLib.Services.QualitySize;
using Recyclarr.TrashLib.Services.ReleaseProfile;
@ -35,7 +34,7 @@ public class SonarrProcessor : IServiceProcessor
public async Task Process(ISyncSettings settings)
{
// Any compatibility failures will be thrown as exceptions
_compatibilityEnforcer.Check(_config);
await _compatibilityEnforcer.Check(_config);
var didWork = false;
@ -47,13 +46,13 @@ public class SonarrProcessor : IServiceProcessor
if (_config.QualityDefinition != null)
{
await _qualityUpdater.Process(settings.Preview, _config.QualityDefinition, SupportedServices.Sonarr);
await _qualityUpdater.Process(settings.Preview, _config);
didWork = true;
}
if (_config.CustomFormats.Count > 0)
{
await _cfUpdater.Process(settings.Preview, _config.CustomFormats, SupportedServices.Sonarr);
await _cfUpdater.Process(settings.Preview, _config);
didWork = true;
}

@ -1,7 +1,12 @@
using Recyclarr.TrashLib.Config.Services;
namespace Recyclarr.TrashLib.Services.QualitySize.Api;
public interface IQualityDefinitionService
{
Task<List<ServiceQualityDefinitionItem>> GetQualityDefinition();
Task<IList<ServiceQualityDefinitionItem>> UpdateQualityDefinition(IList<ServiceQualityDefinitionItem> newQuality);
Task<List<ServiceQualityDefinitionItem>> GetQualityDefinition(IServiceConfiguration config);
Task<IList<ServiceQualityDefinitionItem>> UpdateQualityDefinition(
IServiceConfiguration config,
IList<ServiceQualityDefinitionItem> newQuality);
}

@ -1,4 +1,5 @@
using Flurl.Http;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Http;
namespace Recyclarr.TrashLib.Services.QualitySize.Api;
@ -12,16 +13,17 @@ internal class QualityDefinitionService : IQualityDefinitionService
_service = service;
}
public async Task<List<ServiceQualityDefinitionItem>> GetQualityDefinition()
public async Task<List<ServiceQualityDefinitionItem>> GetQualityDefinition(IServiceConfiguration config)
{
return await _service.Request("qualitydefinition")
return await _service.Request(config, "qualitydefinition")
.GetJsonAsync<List<ServiceQualityDefinitionItem>>();
}
public async Task<IList<ServiceQualityDefinitionItem>> UpdateQualityDefinition(
IServiceConfiguration config,
IList<ServiceQualityDefinitionItem> newQuality)
{
return await _service.Request("qualityDefinition", "update")
return await _service.Request(config, "qualityDefinition", "update")
.PutJsonAsync(newQuality)
.ReceiveJson<List<ServiceQualityDefinitionItem>>();
}

@ -1,9 +1,8 @@
using Recyclarr.TrashLib.Config;
using Recyclarr.TrashLib.Config.Services;
namespace Recyclarr.TrashLib.Services.QualitySize;
public interface IQualitySizeUpdater
{
Task Process(bool isPreview, QualityDefinitionConfig config, SupportedServices serviceType);
Task Process(bool isPreview, IServiceConfiguration config);
}

@ -1,5 +1,4 @@
using Recyclarr.Common.Extensions;
using Recyclarr.TrashLib.Config;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Services.QualitySize.Api;
using Recyclarr.TrashLib.Services.QualitySize.Guide;
@ -26,40 +25,45 @@ internal class QualitySizeUpdater : IQualitySizeUpdater
_guide = guide;
}
public async Task Process(bool isPreview, QualityDefinitionConfig config, SupportedServices serviceType)
public async Task Process(bool isPreview, IServiceConfiguration config)
{
_log.Information("Processing Quality Definition: {QualityDefinition}", config.Type);
var qualityDefinitions = _guide.GetQualitySizeData(serviceType);
var qualityTypeInConfig = config.Type;
var qualityDef = config.QualityDefinition;
if (qualityDef is null)
{
return;
}
var qualityDefinitions = _guide.GetQualitySizeData(config.ServiceType);
var selectedQuality = qualityDefinitions
.FirstOrDefault(x => x.Type.EqualsIgnoreCase(qualityTypeInConfig));
.FirstOrDefault(x => x.Type.EqualsIgnoreCase(qualityDef.Type));
if (selectedQuality == null)
{
_log.Error("The specified quality definition type does not exist: {Type}", qualityTypeInConfig);
_log.Error("The specified quality definition type does not exist: {Type}", qualityDef.Type);
return;
}
if (config.PreferredRatio is not null)
_log.Information("Processing Quality Definition: {QualityDefinition}", qualityDef.Type);
if (qualityDef.PreferredRatio is not null)
{
_log.Information("Using an explicit preferred ratio which will override values from the guide");
// Fix an out of range ratio and warn the user
if (config.PreferredRatio is < 0 or > 1)
if (qualityDef.PreferredRatio is < 0 or > 1)
{
var clampedRatio = Math.Clamp(config.PreferredRatio.Value, 0, 1);
var clampedRatio = Math.Clamp(qualityDef.PreferredRatio.Value, 0, 1);
_log.Warning("Your `preferred_ratio` of {CurrentRatio} is out of range. " +
"It must be a decimal between 0.0 and 1.0. It has been clamped to {ClampedRatio}",
config.PreferredRatio, clampedRatio);
qualityDef.PreferredRatio, clampedRatio);
config.PreferredRatio = clampedRatio;
qualityDef.PreferredRatio = clampedRatio;
}
// Apply a calculated preferred size
foreach (var quality in selectedQuality.Qualities)
{
quality.Preferred = quality.InterpolatedPreferred(config.PreferredRatio.Value);
quality.Preferred = quality.InterpolatedPreferred(qualityDef.PreferredRatio.Value);
}
}
@ -69,7 +73,7 @@ internal class QualitySizeUpdater : IQualitySizeUpdater
return;
}
await ProcessQualityDefinition(selectedQuality.Qualities);
await ProcessQualityDefinition(config, selectedQuality.Qualities);
}
private void PrintQualityPreview(IReadOnlyCollection<QualitySizeItem> qualitySizeItems)
@ -98,9 +102,11 @@ internal class QualitySizeUpdater : IQualitySizeUpdater
a.PreferredSize is not null && b.IsPreferredDifferent(a.PreferredSize);
}
private async Task ProcessQualityDefinition(IReadOnlyCollection<QualitySizeItem> guideQuality)
private async Task ProcessQualityDefinition(
IServiceConfiguration config,
IReadOnlyCollection<QualitySizeItem> guideQuality)
{
var serverQuality = await _api.GetQualityDefinition();
var serverQuality = await _api.GetQualityDefinition(config);
var newQuality = new List<ServiceQualityDefinitionItem>();
foreach (var qualityData in guideQuality)
@ -129,7 +135,7 @@ internal class QualitySizeUpdater : IQualitySizeUpdater
serverEntry.PreferredSize);
}
await _api.UpdateQualityDefinition(newQuality);
await _api.UpdateQualityDefinition(config, newQuality);
_log.Information("Number of updated qualities: {Count}", newQuality.Count);
}
}

@ -1,6 +0,0 @@
namespace Recyclarr.TrashLib.Services.Radarr;
public interface IRadarrCapabilityChecker
{
RadarrCapabilities? GetCapabilities();
}

@ -6,7 +6,6 @@ public class RadarrAutofacModule : Module
{
protected override void Load(ContainerBuilder builder)
{
builder.RegisterType<RadarrCapabilityChecker>().As<IRadarrCapabilityChecker>()
.InstancePerLifetimeScope();
builder.RegisterType<RadarrCapabilityChecker>().InstancePerLifetimeScope();
}
}

@ -3,7 +3,7 @@ using Recyclarr.TrashLib.Services.System;
namespace Recyclarr.TrashLib.Services.Radarr;
public class RadarrCapabilityChecker : ServiceCapabilityChecker<RadarrCapabilities>, IRadarrCapabilityChecker
public class RadarrCapabilityChecker : ServiceCapabilityChecker<RadarrCapabilities>
{
public RadarrCapabilityChecker(IServiceInformation info)
: base(info)

@ -1,11 +1,12 @@
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Services.ReleaseProfile.Api.Objects;
namespace Recyclarr.TrashLib.Services.ReleaseProfile.Api;
public interface IReleaseProfileApiService
{
Task UpdateReleaseProfile(SonarrReleaseProfile profile);
Task<SonarrReleaseProfile> CreateReleaseProfile(SonarrReleaseProfile profile);
Task<IList<SonarrReleaseProfile>> GetReleaseProfiles();
Task DeleteReleaseProfile(int releaseProfileId);
Task UpdateReleaseProfile(IServiceConfiguration config, SonarrReleaseProfile profile);
Task<SonarrReleaseProfile> CreateReleaseProfile(IServiceConfiguration config, SonarrReleaseProfile profile);
Task<IList<SonarrReleaseProfile>> GetReleaseProfiles(IServiceConfiguration config);
Task DeleteReleaseProfile(IServiceConfiguration config, int releaseProfileId);
}

@ -1,10 +1,13 @@
using Newtonsoft.Json.Linq;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Services.ReleaseProfile.Api.Objects;
namespace Recyclarr.TrashLib.Services.ReleaseProfile.Api;
public interface ISonarrReleaseProfileCompatibilityHandler
{
object CompatibleReleaseProfileForSending(SonarrReleaseProfile profile);
Task<object> CompatibleReleaseProfileForSending(IServiceConfiguration config,
SonarrReleaseProfile profile);
SonarrReleaseProfile CompatibleReleaseProfileForReceiving(JObject profile);
}

@ -1,5 +1,6 @@
using Flurl.Http;
using Newtonsoft.Json.Linq;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Http;
using Recyclarr.TrashLib.Services.ReleaseProfile.Api.Objects;
@ -18,27 +19,29 @@ public class ReleaseProfileApiService : IReleaseProfileApiService
_service = service;
}
public async Task UpdateReleaseProfile(SonarrReleaseProfile profile)
public async Task UpdateReleaseProfile(IServiceConfiguration config, SonarrReleaseProfile profile)
{
var profileToSend = _profileHandler.CompatibleReleaseProfileForSending(profile);
await _service.Request("releaseprofile", profile.Id)
var profileToSend = await _profileHandler.CompatibleReleaseProfileForSending(config, profile);
await _service.Request(config, "releaseprofile", profile.Id)
.PutJsonAsync(profileToSend);
}
public async Task<SonarrReleaseProfile> CreateReleaseProfile(SonarrReleaseProfile profile)
public async Task<SonarrReleaseProfile> CreateReleaseProfile(
IServiceConfiguration config,
SonarrReleaseProfile profile)
{
var profileToSend = _profileHandler.CompatibleReleaseProfileForSending(profile);
var profileToSend = _profileHandler.CompatibleReleaseProfileForSending(config, profile);
var response = await _service.Request("releaseprofile")
var response = await _service.Request(config, "releaseprofile")
.PostJsonAsync(profileToSend)
.ReceiveJson<JObject>();
return _profileHandler.CompatibleReleaseProfileForReceiving(response);
}
public async Task<IList<SonarrReleaseProfile>> GetReleaseProfiles()
public async Task<IList<SonarrReleaseProfile>> GetReleaseProfiles(IServiceConfiguration config)
{
var response = await _service.Request("releaseprofile")
var response = await _service.Request(config, "releaseprofile")
.GetJsonAsync<List<JObject>>();
return response
@ -46,9 +49,9 @@ public class ReleaseProfileApiService : IReleaseProfileApiService
.ToList();
}
public async Task DeleteReleaseProfile(int releaseProfileId)
public async Task DeleteReleaseProfile(IServiceConfiguration config, int releaseProfileId)
{
await _service.Request("releaseprofile", releaseProfileId)
await _service.Request(config, "releaseprofile", releaseProfileId)
.DeleteAsync();
}
}

@ -1,6 +1,7 @@
using AutoMapper;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Schema;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.ExceptionTypes;
using Recyclarr.TrashLib.Services.ReleaseProfile.Api.Objects;
using Recyclarr.TrashLib.Services.ReleaseProfile.Api.Schemas;
@ -24,9 +25,11 @@ public class SonarrReleaseProfileCompatibilityHandler : ISonarrReleaseProfileCom
_mapper = mapper;
}
public object CompatibleReleaseProfileForSending(SonarrReleaseProfile profile)
public async Task<object> CompatibleReleaseProfileForSending(
IServiceConfiguration config,
SonarrReleaseProfile profile)
{
var capabilities = _capabilityChecker.GetCapabilities();
var capabilities = await _capabilityChecker.GetCapabilities(config);
if (capabilities is null)
{
throw new ServiceIncompatibilityException("Capabilities could not be obtained");

@ -1,4 +1,5 @@
using Recyclarr.Common.Extensions;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Services.ReleaseProfile.Api;
using Recyclarr.TrashLib.Services.ReleaseProfile.Api.Objects;
using Recyclarr.TrashLib.Services.ReleaseProfile.Filters;
@ -65,7 +66,7 @@ public class ReleaseProfileUpdater : IReleaseProfileUpdater
return;
}
await ProcessReleaseProfiles(filteredProfiles);
await ProcessReleaseProfiles(config, filteredProfiles);
}
private void PreviewReleaseProfiles(IEnumerable<ReleaseProfileData> profiles)
@ -139,40 +140,42 @@ public class ReleaseProfileUpdater : IReleaseProfileUpdater
}
private async Task ProcessReleaseProfiles(
IServiceConfiguration config,
List<(ReleaseProfileData Profile, IReadOnlyCollection<string> Tags)> profilesAndTags)
{
// Obtain all of the existing release profiles first. If any were previously created by our program
// here, we favor replacing those instead of creating new ones, which would just be mostly duplicates
// (but with some differences, since there have likely been updates since the last run).
var existingProfiles = await _releaseProfileApi.GetReleaseProfiles();
var existingProfiles = await _releaseProfileApi.GetReleaseProfiles(config);
foreach (var (profile, tags) in profilesAndTags)
{
// If tags were provided, ensure they exist. Tags that do not exist are added first, so that we
// may specify them with the release profile request payload.
var tagIds = await CreateTagsInSonarr(tags);
var tagIds = await CreateTagsInSonarr(config, tags);
var title = BuildProfileTitle(profile.Name);
var profileToUpdate = GetProfileToUpdate(existingProfiles, title);
if (profileToUpdate != null)
{
_log.Information("Update existing profile: {ProfileName}", title);
await UpdateExistingProfile(profileToUpdate, profile, tagIds);
await UpdateExistingProfile(config, profileToUpdate, profile, tagIds);
}
else
{
_log.Information("Create new profile: {ProfileName}", title);
await CreateNewProfile(title, profile, tagIds);
await CreateNewProfile(config, title, profile, tagIds);
}
}
// Any profiles with `[Trash]` in front of their name are managed exclusively by Recyclarr. As such, if
// there are any still in Sonarr that we didn't update, those are most certainly old and shouldn't be kept
// around anymore.
await DeleteOldManagedProfiles(profilesAndTags, existingProfiles);
await DeleteOldManagedProfiles(config, profilesAndTags, existingProfiles);
}
private async Task DeleteOldManagedProfiles(
IServiceConfiguration config,
IEnumerable<(ReleaseProfileData Profile, IReadOnlyCollection<string> Tags)> profilesAndTags,
IEnumerable<SonarrReleaseProfile> sonarrProfiles)
{
@ -187,32 +190,37 @@ public class ReleaseProfileUpdater : IReleaseProfileUpdater
foreach (var profile in sonarrProfilesToDelete)
{
_log.Information("Deleting old Trash release profile: {ProfileName}", profile.Name);
await _releaseProfileApi.DeleteReleaseProfile(profile.Id);
await _releaseProfileApi.DeleteReleaseProfile(config, profile.Id);
}
}
private async Task<IReadOnlyCollection<int>> CreateTagsInSonarr(IReadOnlyCollection<string> tags)
private async Task<IReadOnlyCollection<int>> CreateTagsInSonarr(
IServiceConfiguration config,
IReadOnlyCollection<string> tags)
{
if (!tags.Any())
{
return Array.Empty<int>();
}
var sonarrTags = await _tagApiService.GetTags();
await CreateMissingTags(sonarrTags, tags);
var sonarrTags = await _tagApiService.GetTags(config);
await CreateMissingTags(config, sonarrTags, tags);
return sonarrTags
.Where(t => tags.Any(ct => ct.EqualsIgnoreCase(t.Label)))
.Select(t => t.Id)
.ToList();
}
private async Task CreateMissingTags(ICollection<SonarrTag> sonarrTags, IEnumerable<string> configTags)
private async Task CreateMissingTags(
IServiceConfiguration config,
ICollection<SonarrTag> sonarrTags,
IEnumerable<string> configTags)
{
var missingTags = configTags.Where(t => !sonarrTags.Any(t2 => t2.Label.EqualsIgnoreCase(t)));
foreach (var tag in missingTags)
{
_log.Debug("Creating Tag: {Tag}", tag);
var newTag = await _tagApiService.CreateTag(tag);
var newTag = await _tagApiService.CreateTag(config, tag);
sonarrTags.Add(newTag);
}
}
@ -243,18 +251,25 @@ public class ReleaseProfileUpdater : IReleaseProfileUpdater
profileToUpdate.Tags = tagIds;
}
private async Task UpdateExistingProfile(SonarrReleaseProfile profileToUpdate, ReleaseProfileData profile,
private async Task UpdateExistingProfile(
IServiceConfiguration config,
SonarrReleaseProfile profileToUpdate,
ReleaseProfileData profile,
IReadOnlyCollection<int> tagIds)
{
_log.Debug("Update existing profile with id {ProfileId}", profileToUpdate.Id);
SetupProfileRequestObject(profileToUpdate, profile, tagIds);
await _releaseProfileApi.UpdateReleaseProfile(profileToUpdate);
await _releaseProfileApi.UpdateReleaseProfile(config, profileToUpdate);
}
private async Task CreateNewProfile(string title, ReleaseProfileData profile, IReadOnlyCollection<int> tagIds)
private async Task CreateNewProfile(
IServiceConfiguration config,
string title,
ReleaseProfileData profile,
IReadOnlyCollection<int> tagIds)
{
var newProfile = new SonarrReleaseProfile {Name = title, Enabled = true};
SetupProfileRequestObject(newProfile, profile, tagIds);
await _releaseProfileApi.CreateReleaseProfile(newProfile);
await _releaseProfileApi.CreateReleaseProfile(config, newProfile);
}
}

@ -1,9 +1,10 @@
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Services.Sonarr.Api.Objects;
namespace Recyclarr.TrashLib.Services.Sonarr.Api;
public interface ISonarrTagApiService
{
Task<IList<SonarrTag>> GetTags();
Task<SonarrTag> CreateTag(string tag);
Task<IList<SonarrTag>> GetTags(IServiceConfiguration config);
Task<SonarrTag> CreateTag(IServiceConfiguration config, string tag);
}

@ -1,4 +1,5 @@
using Flurl.Http;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Http;
using Recyclarr.TrashLib.Services.Sonarr.Api.Objects;
@ -13,15 +14,15 @@ public class SonarrTagApiService : ISonarrTagApiService
_service = service;
}
public async Task<IList<SonarrTag>> GetTags()
public async Task<IList<SonarrTag>> GetTags(IServiceConfiguration config)
{
return await _service.Request("tag")
return await _service.Request(config, "tag")
.GetJsonAsync<List<SonarrTag>>();
}
public async Task<SonarrTag> CreateTag(string tag)
public async Task<SonarrTag> CreateTag(IServiceConfiguration config, string tag)
{
return await _service.Request("tag")
return await _service.Request(config, "tag")
.PostJsonAsync(new {label = tag})
.ReceiveJson<SonarrTag>();
}

@ -1,6 +1,8 @@
using Recyclarr.TrashLib.Config.Services;
namespace Recyclarr.TrashLib.Services.Sonarr.Capabilities;
public interface ISonarrCapabilityChecker
{
SonarrCapabilities? GetCapabilities();
Task<SonarrCapabilities?> GetCapabilities(IServiceConfiguration config);
}

@ -13,9 +13,9 @@ public class SonarrCapabilityEnforcer
_capabilityChecker = capabilityChecker;
}
public void Check(SonarrConfiguration config)
public async Task Check(SonarrConfiguration config)
{
var capabilities = _capabilityChecker.GetCapabilities();
var capabilities = await _capabilityChecker.GetCapabilities(config);
if (capabilities is null)
{
throw new ServiceIncompatibilityException("Capabilities could not be obtained");

@ -1,6 +1,8 @@
using Recyclarr.TrashLib.Config.Services;
namespace Recyclarr.TrashLib.Services.System;
public interface IServiceInformation
{
IObservable<Version?> Version { get; }
public Task<Version?> GetVersion(IServiceConfiguration config);
}

@ -1,8 +1,9 @@
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Services.System.Dto;
namespace Recyclarr.TrashLib.Services.System;
public interface ISystemApiService
{
Task<SystemStatus> GetStatus();
Task<SystemStatus> GetStatus(IServiceConfiguration config);
}

@ -1,40 +1,33 @@
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using Flurl.Http;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Http;
using Recyclarr.TrashLib.Services.System.Dto;
namespace Recyclarr.TrashLib.Services.System;
public class ServiceInformation : IServiceInformation
{
private readonly ISystemApiService _api;
private readonly ILogger _log;
public ServiceInformation(ISystemApiService api, ILogger log)
{
_api = api;
_log = log;
Version = Observable.FromAsync(async () => await api.GetStatus(), ThreadPoolScheduler.Instance)
.Timeout(TimeSpan.FromSeconds(15))
.Do(LogServiceInfo)
.Select(x => new Version(x.Version))
.Catch((Exception ex) =>
{
log.Error("Exception trying to obtain service version: {Message}", ex switch
{
FlurlHttpException flex => flex.SanitizedExceptionMessage(),
_ => ex.Message
});
return Observable.Return((Version?) null);
})
.Replay(1)
.AutoConnect();
}
public IObservable<Version?> Version { get; }
private void LogServiceInfo(SystemStatus status)
public async Task<Version?> GetVersion(IServiceConfiguration config)
{
_log.Debug("{Service} Version: {Version}", status.AppName, status.Version);
try
{
var status = await _api.GetStatus(config);
_log.Debug("{Service} Version: {Version}", status.AppName, status.Version);
return new Version(status.Version);
}
catch (FlurlHttpException ex)
{
_log.Error("Exception trying to obtain service version: {Message}", ex.SanitizedExceptionMessage());
}
return null;
}
}

@ -1,4 +1,5 @@
using Flurl.Http;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Http;
using Recyclarr.TrashLib.Services.System.Dto;
@ -13,9 +14,9 @@ public class SystemApiService : ISystemApiService
_service = service;
}
public async Task<SystemStatus> GetStatus()
public async Task<SystemStatus> GetStatus(IServiceConfiguration config)
{
return await _service.Request("system", "status")
return await _service.Request(config, "system", "status")
.GetJsonAsync<SystemStatus>();
}
}

@ -112,5 +112,6 @@
&amp;lt;/Language&amp;gt;&#xD;
&amp;lt;/profile&amp;gt;&lt;/RIDER_SETTINGS&gt;&lt;CSharpFormatDocComments&gt;True&lt;/CSharpFormatDocComments&gt;&lt;XAMLCollapseEmptyTags&gt;False&lt;/XAMLCollapseEmptyTags&gt;&lt;RemoveCodeRedundancies&gt;True&lt;/RemoveCodeRedundancies&gt;&lt;CSMakeFieldReadonly&gt;True&lt;/CSMakeFieldReadonly&gt;&lt;/Profile&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/CodeCleanup/SilentCleanupProfile/@EntryValue">Recyclarr Cleanup</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Persister/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=radarr/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Sonarr/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
Loading…
Cancel
Save