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

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

@ -11,39 +11,36 @@ namespace Recyclarr.Cli.Console.Helpers;
public class CacheStoragePath : ICacheStoragePath public class CacheStoragePath : ICacheStoragePath
{ {
private readonly IAppPaths _paths; private readonly IAppPaths _paths;
private readonly IServiceConfiguration _config;
private readonly IFNV1a _hash; private readonly IFNV1a _hash;
public CacheStoragePath( public CacheStoragePath(
IAppPaths paths, IAppPaths paths)
IServiceConfiguration config)
{ {
_paths = paths; _paths = paths;
_config = config;
_hash = FNV1aFactory.Instance.Create(FNVConfig.GetPredefinedConfig(32)); _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 // 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. // and the below condition can be removed and the logic simplified.
var dirName = new StringBuilder(); 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(); var guid = _hash.ComputeHash(Encoding.ASCII.GetBytes(url)).AsHexString();
dirName.Append(guid); dirName.Append(guid);
return dirName.ToString(); return dirName.ToString();
} }
public IFileInfo CalculatePath(string cacheObjectName) public IFileInfo CalculatePath(IServiceConfiguration config, string cacheObjectName)
{ {
return _paths.CacheDirectory return _paths.CacheDirectory
.SubDirectory(_config.ServiceType.ToString().ToLower(CultureInfo.CurrentCulture)) .SubDirectory(config.ServiceType.ToString().ToLower(CultureInfo.CurrentCulture))
.SubDirectory(BuildUniqueServiceDir()) .SubDirectory(BuildUniqueServiceDir(config))
.File(cacheObjectName + ".json"); .File(cacheObjectName + ".json");
} }
} }

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

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

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

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

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

@ -21,11 +21,11 @@ public class SonarrCapabilityEnforcerTest
{ {
var config = new SonarrConfiguration(); var config = new SonarrConfiguration();
checker.GetCapabilities().Returns((SonarrCapabilities?) null); checker.GetCapabilities(default!).ReturnsForAnyArgs((SonarrCapabilities?) null);
var act = () => sut.Check(config); var act = () => sut.Check(config);
act.Should().Throw<ServiceIncompatibilityException>().WithMessage("*obtained*"); act.Should().ThrowAsync<ServiceIncompatibilityException>().WithMessage("*obtained*");
} }
[Test, AutoMockData] [Test, AutoMockData]
@ -35,14 +35,14 @@ public class SonarrCapabilityEnforcerTest
{ {
var config = new SonarrConfiguration(); var config = new SonarrConfiguration();
checker.GetCapabilities().Returns(new SonarrCapabilities(new Version()) checker.GetCapabilities(default!).ReturnsForAnyArgs(new SonarrCapabilities(new Version())
{ {
SupportsNamedReleaseProfiles = false SupportsNamedReleaseProfiles = false
}); });
var act = () => sut.Check(config); var act = () => sut.Check(config);
act.Should().Throw<ServiceIncompatibilityException>().WithMessage("*minimum*"); act.Should().ThrowAsync<ServiceIncompatibilityException>().WithMessage("*minimum*");
} }
[Test, AutoMockData] [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, SupportsNamedReleaseProfiles = true,
SupportsCustomFormats = true SupportsCustomFormats = true
@ -66,7 +66,7 @@ public class SonarrCapabilityEnforcerTest
var act = () => sut.Check(config); var act = () => sut.Check(config);
act.Should().Throw<ServiceIncompatibilityException>().WithMessage("*v3*"); act.Should().ThrowAsync<ServiceIncompatibilityException>().WithMessage("*v3*");
} }
[Test, AutoMockData] [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, SupportsNamedReleaseProfiles = true,
SupportsCustomFormats = false SupportsCustomFormats = false
@ -90,6 +90,6 @@ public class SonarrCapabilityEnforcerTest
var act = () => sut.Check(config); 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 NUnit.Framework;
using Recyclarr.Cli.TestLibrary; using Recyclarr.Cli.TestLibrary;
using Recyclarr.TestLibrary; using Recyclarr.TestLibrary;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Services.ReleaseProfile.Api; using Recyclarr.TrashLib.Services.ReleaseProfile.Api;
using Recyclarr.TrashLib.Services.ReleaseProfile.Api.Objects; using Recyclarr.TrashLib.Services.ReleaseProfile.Api.Objects;
using Recyclarr.TrashLib.Services.Sonarr.Capabilities; using Recyclarr.TrashLib.Services.Sonarr.Capabilities;
@ -58,10 +59,10 @@ public class SonarrCompatibilityTest : IntegrationFixture
} }
[Test] [Test]
public void Send_v2_to_v1() public async Task Send_v2_to_v1()
{ {
var capabilityChecker = Resolve<ISonarrCapabilityChecker>(); var capabilityChecker = Resolve<ISonarrCapabilityChecker>();
capabilityChecker.GetCapabilities().Returns(new SonarrCapabilities(new Version()) capabilityChecker.GetCapabilities(default!).ReturnsForAnyArgs(new SonarrCapabilities(new Version())
{ {
ArraysNeededForReleaseProfileRequiredAndIgnored = false ArraysNeededForReleaseProfileRequiredAndIgnored = false
}); });
@ -69,16 +70,16 @@ public class SonarrCompatibilityTest : IntegrationFixture
var sut = Resolve<SonarrReleaseProfileCompatibilityHandler>(); var sut = Resolve<SonarrReleaseProfileCompatibilityHandler>();
var data = new SonarrReleaseProfile {Ignored = new List<string> {"one", "two", "three"}}; 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"}); result.Should().BeEquivalentTo(new SonarrReleaseProfileV1 {Ignored = "one,two,three"});
} }
[Test] [Test]
public void Send_v2_to_v2() public async Task Send_v2_to_v2()
{ {
var capabilityChecker = Resolve<ISonarrCapabilityChecker>(); var capabilityChecker = Resolve<ISonarrCapabilityChecker>();
capabilityChecker.GetCapabilities().Returns(new SonarrCapabilities(new Version()) capabilityChecker.GetCapabilities(default!).ReturnsForAnyArgs(new SonarrCapabilities(new Version())
{ {
ArraysNeededForReleaseProfileRequiredAndIgnored = true ArraysNeededForReleaseProfileRequiredAndIgnored = true
}); });
@ -86,7 +87,7 @@ public class SonarrCompatibilityTest : IntegrationFixture
var sut = Resolve<SonarrReleaseProfileCompatibilityHandler>(); var sut = Resolve<SonarrReleaseProfileCompatibilityHandler>();
var data = new SonarrReleaseProfile {Ignored = new List<string> {"one", "two", "three"}}; 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); result.Should().BeEquivalentTo(data);
} }

@ -1,8 +1,9 @@
using System.IO.Abstractions; using System.IO.Abstractions;
using Recyclarr.TrashLib.Config.Services;
namespace Recyclarr.TrashLib.Cache; namespace Recyclarr.TrashLib.Cache;
public interface ICacheStoragePath 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; namespace Recyclarr.TrashLib.Cache;
public interface IServiceCache public interface IServiceCache
{ {
T? Load<T>() where T : class; T? Load<T>(IServiceConfiguration config) where T : class;
void Save<T>(T obj) 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;
using Newtonsoft.Json.Serialization; using Newtonsoft.Json.Serialization;
using Recyclarr.Common.Extensions; using Recyclarr.Common.Extensions;
using Recyclarr.TrashLib.Config.Services;
namespace Recyclarr.TrashLib.Cache; namespace Recyclarr.TrashLib.Cache;
@ -29,9 +30,9 @@ public class ServiceCache : IServiceCache
private ILogger Log { get; } 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) if (!path.Exists)
{ {
return null; return null;
@ -52,9 +53,9 @@ public class ServiceCache : IServiceCache
return null; 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(); path.CreateParentDirectory();
var serializer = JsonSerializer.Create(_jsonSettings); var serializer = JsonSerializer.Create(_jsonSettings);
@ -74,7 +75,7 @@ public class ServiceCache : IServiceCache
return attribute.Name; return attribute.Name;
} }
private IFileInfo PathFromAttribute<T>() private IFileInfo PathFromAttribute<T>(IServiceConfiguration config)
{ {
var objectName = GetCacheObjectNameAttribute<T>(); var objectName = GetCacheObjectNameAttribute<T>();
if (!AllowedObjectNameCharacters.IsMatch(objectName)) if (!AllowedObjectNameCharacters.IsMatch(objectName))
@ -82,6 +83,6 @@ public class ServiceCache : IServiceCache
throw new ArgumentException($"Object name '{objectName}' has unacceptable characters"); 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; } Uri BaseUrl { get; }
string ApiKey { get; } string ApiKey { get; }
bool DeleteOldCustomFormats { get; } bool DeleteOldCustomFormats { get; }
ICollection<CustomFormatConfig> CustomFormats { get; init; }
QualityDefinitionConfig? QualityDefinition { get; init; }
} }

@ -1,8 +1,9 @@
using Flurl.Http; using Flurl.Http;
using Recyclarr.TrashLib.Config.Services;
namespace Recyclarr.TrashLib.Http; namespace Recyclarr.TrashLib.Http;
public interface IServiceRequestBuilder 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 public class ServiceRequestBuilder : IServiceRequestBuilder
{ {
private readonly IServiceConfiguration _config;
private readonly IFlurlClientFactory _clientFactory; private readonly IFlurlClientFactory _clientFactory;
public ServiceRequestBuilder(IServiceConfiguration config, IFlurlClientFactory clientFactory) public ServiceRequestBuilder(IFlurlClientFactory clientFactory)
{ {
_config = config;
_clientFactory = clientFactory; _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()) 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; using Recyclarr.TrashLib.Services.System;
namespace Recyclarr.TrashLib.Services.Common; namespace Recyclarr.TrashLib.Services.Common;
public abstract class ServiceCapabilityChecker<T> where T : class 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 var version = await _info.GetVersion(config);
.Select(x => x is null ? null : BuildCapabilitiesObject(x)) return version is not null ? BuildCapabilitiesObject(version) : null;
.Replay(1)
.AutoConnect()
.LastAsync();
} }
protected abstract T BuildCapabilitiesObject(Version version); protected abstract T BuildCapabilitiesObject(Version version);

@ -1,5 +1,6 @@
using Flurl.Http; using Flurl.Http;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Http; using Recyclarr.TrashLib.Http;
using Recyclarr.TrashLib.Services.CustomFormat.Models; using Recyclarr.TrashLib.Services.CustomFormat.Models;
@ -14,15 +15,15 @@ internal class CustomFormatService : ICustomFormatService
_service = service; _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>>(); .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) .PostJsonAsync(cf.Json)
.ReceiveJson<JObject>(); .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) .PutJsonAsync(cf.Json)
.ReceiveJson<JObject>(); .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(); .DeleteAsync();
} }
} }

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

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

@ -1,5 +1,6 @@
using Flurl.Http; using Flurl.Http;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Http; using Recyclarr.TrashLib.Http;
namespace Recyclarr.TrashLib.Services.CustomFormat.Api; namespace Recyclarr.TrashLib.Services.CustomFormat.Api;
@ -13,15 +14,15 @@ internal class QualityProfileService : IQualityProfileService
_service = service; _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>>(); .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) .PutJsonAsync(profileJson)
.ReceiveJson<JObject>(); .ReceiveJson<JObject>();
} }

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

@ -1,5 +1,4 @@
using Recyclarr.Common.Extensions; using Recyclarr.Common.Extensions;
using Recyclarr.TrashLib.Config;
using Recyclarr.TrashLib.Config.Services; using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Services.CustomFormat.Processors; using Recyclarr.TrashLib.Services.CustomFormat.Processors;
using Recyclarr.TrashLib.Services.CustomFormat.Processors.PersistenceSteps; using Recyclarr.TrashLib.Services.CustomFormat.Processors.PersistenceSteps;
@ -29,11 +28,11 @@ internal class CustomFormatUpdater : ICustomFormatUpdater
_console = console; _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()) if (!ValidateGuideDataAndCheckShouldProceed())
{ {
@ -48,6 +47,7 @@ internal class CustomFormatUpdater : ICustomFormatUpdater
} }
await _persistenceProcessor.PersistCustomFormats( await _persistenceProcessor.PersistCustomFormats(
config,
_guideProcessor.ProcessedCustomFormats, _guideProcessor.ProcessedCustomFormats,
_guideProcessor.DeletedCustomFormatsInCache, _guideProcessor.DeletedCustomFormatsInCache,
_guideProcessor.ProfileScores); _guideProcessor.ProfileScores);
@ -57,7 +57,7 @@ internal class CustomFormatUpdater : ICustomFormatUpdater
// Cache all the custom formats (using ID from API response). // Cache all the custom formats (using ID from API response).
_cache.Update(_guideProcessor.ProcessedCustomFormats); _cache.Update(_guideProcessor.ProcessedCustomFormats);
_cache.Save(); _cache.Save(config);
} }
private void PrintQualityProfileUpdates() private void PrintQualityProfileUpdates()

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

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

@ -15,20 +15,14 @@ public interface IPersistenceProcessorSteps
internal class PersistenceProcessor : IPersistenceProcessor internal class PersistenceProcessor : IPersistenceProcessor
{ {
private readonly IServiceConfiguration _config;
private readonly ICustomFormatService _customFormatService; private readonly ICustomFormatService _customFormatService;
private readonly IQualityProfileService _qualityProfileService;
private readonly IPersistenceProcessorSteps _steps; private readonly IPersistenceProcessorSteps _steps;
public PersistenceProcessor( public PersistenceProcessor(
ICustomFormatService customFormatService, ICustomFormatService customFormatService,
IQualityProfileService qualityProfileService,
IServiceConfiguration config,
IPersistenceProcessorSteps steps) IPersistenceProcessorSteps steps)
{ {
_customFormatService = customFormatService; _customFormatService = customFormatService;
_qualityProfileService = qualityProfileService;
_config = config;
_steps = steps; _steps = steps;
} }
@ -42,11 +36,12 @@ internal class PersistenceProcessor : IPersistenceProcessor
=> _steps.ProfileQualityProfileApiPersister.InvalidProfileNames; => _steps.ProfileQualityProfileApiPersister.InvalidProfileNames;
public async Task PersistCustomFormats( public async Task PersistCustomFormats(
IServiceConfiguration config,
IReadOnlyCollection<ProcessedCustomFormatData> guideCfs, IReadOnlyCollection<ProcessedCustomFormatData> guideCfs,
IEnumerable<TrashIdMapping> deletedCfsInCache, IEnumerable<TrashIdMapping> deletedCfsInCache,
IDictionary<string, QualityProfileCustomFormatScoreMapping> profileScores) 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 // 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 // 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); _steps.JsonTransactionStep.Process(guideCfs, serviceCfs);
// Step 1.1: Optionally record deletions of custom formats in cache but not in the guide // 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); _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 // 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. // to existing CFs and creation of brand new ones, depending on what's already available in Radarr.
await _steps.CustomFormatCustomFormatApiPersister.Process(_customFormatService, await _steps.CustomFormatCustomFormatApiPersister.Process(config, _steps.JsonTransactionStep.Transactions);
_steps.JsonTransactionStep.Transactions);
// Step 3: Update all quality profiles with the scores from the guide for the uploaded custom formats // 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; using Recyclarr.TrashLib.Services.CustomFormat.Api;
namespace Recyclarr.TrashLib.Services.CustomFormat.Processors.PersistenceSteps; namespace Recyclarr.TrashLib.Services.CustomFormat.Processors.PersistenceSteps;
internal class CustomFormatApiPersistenceStep : ICustomFormatApiPersistenceStep 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) foreach (var cf in transactions.NewCustomFormats)
{ {
await api.CreateCustomFormat(cf); await _api.CreateCustomFormat(config, cf);
} }
foreach (var cf in transactions.UpdatedCustomFormats) foreach (var cf in transactions.UpdatedCustomFormats)
{ {
await api.UpdateCustomFormat(cf); await _api.UpdateCustomFormat(config, cf);
} }
foreach (var cfId in transactions.DeletedCustomFormatIds) 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; namespace Recyclarr.TrashLib.Services.CustomFormat.Processors.PersistenceSteps;
public interface ICustomFormatApiPersistenceStep 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; using Recyclarr.TrashLib.Services.CustomFormat.Models;
namespace Recyclarr.TrashLib.Services.CustomFormat.Processors.PersistenceSteps; namespace Recyclarr.TrashLib.Services.CustomFormat.Processors.PersistenceSteps;
@ -8,6 +8,7 @@ public interface IQualityProfileApiPersistenceStep
IDictionary<string, List<UpdatedFormatScore>> UpdatedScores { get; } IDictionary<string, List<UpdatedFormatScore>> UpdatedScores { get; }
IReadOnlyCollection<string> InvalidProfileNames { get; } IReadOnlyCollection<string> InvalidProfileNames { get; }
Task Process(IQualityProfileService api, Task Process(
IServiceConfiguration config,
IDictionary<string, QualityProfileCustomFormatScoreMapping> cfScores); IDictionary<string, QualityProfileCustomFormatScoreMapping> cfScores);
} }

@ -1,5 +1,6 @@
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Recyclarr.Common.Extensions; using Recyclarr.Common.Extensions;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Services.CustomFormat.Api; using Recyclarr.TrashLib.Services.CustomFormat.Api;
using Recyclarr.TrashLib.Services.CustomFormat.Models; using Recyclarr.TrashLib.Services.CustomFormat.Models;
@ -7,16 +8,23 @@ namespace Recyclarr.TrashLib.Services.CustomFormat.Processors.PersistenceSteps;
internal class QualityProfileApiPersistenceStep : IQualityProfileApiPersistenceStep internal class QualityProfileApiPersistenceStep : IQualityProfileApiPersistenceStep
{ {
private readonly IQualityProfileService _api;
private readonly List<string> _invalidProfileNames = new(); private readonly List<string> _invalidProfileNames = new();
private readonly Dictionary<string, List<UpdatedFormatScore>> _updatedScores = new(); private readonly Dictionary<string, List<UpdatedFormatScore>> _updatedScores = new();
public IDictionary<string, List<UpdatedFormatScore>> UpdatedScores => _updatedScores; public IDictionary<string, List<UpdatedFormatScore>> UpdatedScores => _updatedScores;
public IReadOnlyCollection<string> InvalidProfileNames => _invalidProfileNames; 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) 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. // 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"). // 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; 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.CustomFormat;
using Recyclarr.TrashLib.Services.QualitySize; using Recyclarr.TrashLib.Services.QualitySize;
using Recyclarr.TrashLib.Services.Radarr.Config; using Recyclarr.TrashLib.Services.Radarr.Config;
@ -7,18 +6,15 @@ namespace Recyclarr.TrashLib.Services.Processors;
public class RadarrProcessor : IServiceProcessor public class RadarrProcessor : IServiceProcessor
{ {
private readonly ILogger _log;
private readonly ICustomFormatUpdater _cfUpdater; private readonly ICustomFormatUpdater _cfUpdater;
private readonly IQualitySizeUpdater _qualityUpdater; private readonly IQualitySizeUpdater _qualityUpdater;
private readonly RadarrConfiguration _config; private readonly RadarrConfiguration _config;
public RadarrProcessor( public RadarrProcessor(
ILogger log,
ICustomFormatUpdater cfUpdater, ICustomFormatUpdater cfUpdater,
IQualitySizeUpdater qualityUpdater, IQualitySizeUpdater qualityUpdater,
RadarrConfiguration config) RadarrConfiguration config)
{ {
_log = log;
_cfUpdater = cfUpdater; _cfUpdater = cfUpdater;
_qualityUpdater = qualityUpdater; _qualityUpdater = qualityUpdater;
_config = config; _config = config;
@ -26,23 +22,14 @@ public class RadarrProcessor : IServiceProcessor
public async Task Process(ISyncSettings settings) public async Task Process(ISyncSettings settings)
{ {
var didWork = false;
if (_config.QualityDefinition != null) if (_config.QualityDefinition != null)
{ {
await _qualityUpdater.Process(settings.Preview, _config.QualityDefinition, SupportedServices.Radarr); await _qualityUpdater.Process(settings.Preview, _config);
didWork = true;
} }
if (_config.CustomFormats.Count > 0) if (_config.CustomFormats.Count > 0)
{ {
await _cfUpdater.Process(settings.Preview, _config.CustomFormats, SupportedServices.Radarr); await _cfUpdater.Process(settings.Preview, _config);
didWork = true;
}
if (!didWork)
{
_log.Information("Nothing to do");
} }
} }
} }

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

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

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

@ -1,9 +1,8 @@
using Recyclarr.TrashLib.Config;
using Recyclarr.TrashLib.Config.Services; using Recyclarr.TrashLib.Config.Services;
namespace Recyclarr.TrashLib.Services.QualitySize; namespace Recyclarr.TrashLib.Services.QualitySize;
public interface IQualitySizeUpdater 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.Common.Extensions;
using Recyclarr.TrashLib.Config;
using Recyclarr.TrashLib.Config.Services; using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Services.QualitySize.Api; using Recyclarr.TrashLib.Services.QualitySize.Api;
using Recyclarr.TrashLib.Services.QualitySize.Guide; using Recyclarr.TrashLib.Services.QualitySize.Guide;
@ -26,40 +25,45 @@ internal class QualitySizeUpdater : IQualitySizeUpdater
_guide = guide; _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 qualityDef = config.QualityDefinition;
var qualityDefinitions = _guide.GetQualitySizeData(serviceType); if (qualityDef is null)
var qualityTypeInConfig = config.Type; {
return;
}
var qualityDefinitions = _guide.GetQualitySizeData(config.ServiceType);
var selectedQuality = qualityDefinitions var selectedQuality = qualityDefinitions
.FirstOrDefault(x => x.Type.EqualsIgnoreCase(qualityTypeInConfig)); .FirstOrDefault(x => x.Type.EqualsIgnoreCase(qualityDef.Type));
if (selectedQuality == null) 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; 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"); _log.Information("Using an explicit preferred ratio which will override values from the guide");
// Fix an out of range ratio and warn the user // 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. " + _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}", "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 // Apply a calculated preferred size
foreach (var quality in selectedQuality.Qualities) 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; return;
} }
await ProcessQualityDefinition(selectedQuality.Qualities); await ProcessQualityDefinition(config, selectedQuality.Qualities);
} }
private void PrintQualityPreview(IReadOnlyCollection<QualitySizeItem> qualitySizeItems) private void PrintQualityPreview(IReadOnlyCollection<QualitySizeItem> qualitySizeItems)
@ -98,9 +102,11 @@ internal class QualitySizeUpdater : IQualitySizeUpdater
a.PreferredSize is not null && b.IsPreferredDifferent(a.PreferredSize); 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>(); var newQuality = new List<ServiceQualityDefinitionItem>();
foreach (var qualityData in guideQuality) foreach (var qualityData in guideQuality)
@ -129,7 +135,7 @@ internal class QualitySizeUpdater : IQualitySizeUpdater
serverEntry.PreferredSize); serverEntry.PreferredSize);
} }
await _api.UpdateQualityDefinition(newQuality); await _api.UpdateQualityDefinition(config, newQuality);
_log.Information("Number of updated qualities: {Count}", newQuality.Count); _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) protected override void Load(ContainerBuilder builder)
{ {
builder.RegisterType<RadarrCapabilityChecker>().As<IRadarrCapabilityChecker>() builder.RegisterType<RadarrCapabilityChecker>().InstancePerLifetimeScope();
.InstancePerLifetimeScope();
} }
} }

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

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

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

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

@ -1,6 +1,7 @@
using AutoMapper; using AutoMapper;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Schema; using Newtonsoft.Json.Schema;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.ExceptionTypes; using Recyclarr.TrashLib.ExceptionTypes;
using Recyclarr.TrashLib.Services.ReleaseProfile.Api.Objects; using Recyclarr.TrashLib.Services.ReleaseProfile.Api.Objects;
using Recyclarr.TrashLib.Services.ReleaseProfile.Api.Schemas; using Recyclarr.TrashLib.Services.ReleaseProfile.Api.Schemas;
@ -24,9 +25,11 @@ public class SonarrReleaseProfileCompatibilityHandler : ISonarrReleaseProfileCom
_mapper = mapper; _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) if (capabilities is null)
{ {
throw new ServiceIncompatibilityException("Capabilities could not be obtained"); throw new ServiceIncompatibilityException("Capabilities could not be obtained");

@ -1,4 +1,5 @@
using Recyclarr.Common.Extensions; using Recyclarr.Common.Extensions;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Services.ReleaseProfile.Api; using Recyclarr.TrashLib.Services.ReleaseProfile.Api;
using Recyclarr.TrashLib.Services.ReleaseProfile.Api.Objects; using Recyclarr.TrashLib.Services.ReleaseProfile.Api.Objects;
using Recyclarr.TrashLib.Services.ReleaseProfile.Filters; using Recyclarr.TrashLib.Services.ReleaseProfile.Filters;
@ -65,7 +66,7 @@ public class ReleaseProfileUpdater : IReleaseProfileUpdater
return; return;
} }
await ProcessReleaseProfiles(filteredProfiles); await ProcessReleaseProfiles(config, filteredProfiles);
} }
private void PreviewReleaseProfiles(IEnumerable<ReleaseProfileData> profiles) private void PreviewReleaseProfiles(IEnumerable<ReleaseProfileData> profiles)
@ -139,40 +140,42 @@ public class ReleaseProfileUpdater : IReleaseProfileUpdater
} }
private async Task ProcessReleaseProfiles( private async Task ProcessReleaseProfiles(
IServiceConfiguration config,
List<(ReleaseProfileData Profile, IReadOnlyCollection<string> Tags)> profilesAndTags) List<(ReleaseProfileData Profile, IReadOnlyCollection<string> Tags)> profilesAndTags)
{ {
// Obtain all of the existing release profiles first. If any were previously created by our program // 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 // 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). // (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) foreach (var (profile, tags) in profilesAndTags)
{ {
// If tags were provided, ensure they exist. Tags that do not exist are added first, so that we // 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. // 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 title = BuildProfileTitle(profile.Name);
var profileToUpdate = GetProfileToUpdate(existingProfiles, title); var profileToUpdate = GetProfileToUpdate(existingProfiles, title);
if (profileToUpdate != null) if (profileToUpdate != null)
{ {
_log.Information("Update existing profile: {ProfileName}", title); _log.Information("Update existing profile: {ProfileName}", title);
await UpdateExistingProfile(profileToUpdate, profile, tagIds); await UpdateExistingProfile(config, profileToUpdate, profile, tagIds);
} }
else else
{ {
_log.Information("Create new profile: {ProfileName}", title); _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 // 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 // there are any still in Sonarr that we didn't update, those are most certainly old and shouldn't be kept
// around anymore. // around anymore.
await DeleteOldManagedProfiles(profilesAndTags, existingProfiles); await DeleteOldManagedProfiles(config, profilesAndTags, existingProfiles);
} }
private async Task DeleteOldManagedProfiles( private async Task DeleteOldManagedProfiles(
IServiceConfiguration config,
IEnumerable<(ReleaseProfileData Profile, IReadOnlyCollection<string> Tags)> profilesAndTags, IEnumerable<(ReleaseProfileData Profile, IReadOnlyCollection<string> Tags)> profilesAndTags,
IEnumerable<SonarrReleaseProfile> sonarrProfiles) IEnumerable<SonarrReleaseProfile> sonarrProfiles)
{ {
@ -187,32 +190,37 @@ public class ReleaseProfileUpdater : IReleaseProfileUpdater
foreach (var profile in sonarrProfilesToDelete) foreach (var profile in sonarrProfilesToDelete)
{ {
_log.Information("Deleting old Trash release profile: {ProfileName}", profile.Name); _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()) if (!tags.Any())
{ {
return Array.Empty<int>(); return Array.Empty<int>();
} }
var sonarrTags = await _tagApiService.GetTags(); var sonarrTags = await _tagApiService.GetTags(config);
await CreateMissingTags(sonarrTags, tags); await CreateMissingTags(config, sonarrTags, tags);
return sonarrTags return sonarrTags
.Where(t => tags.Any(ct => ct.EqualsIgnoreCase(t.Label))) .Where(t => tags.Any(ct => ct.EqualsIgnoreCase(t.Label)))
.Select(t => t.Id) .Select(t => t.Id)
.ToList(); .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))); var missingTags = configTags.Where(t => !sonarrTags.Any(t2 => t2.Label.EqualsIgnoreCase(t)));
foreach (var tag in missingTags) foreach (var tag in missingTags)
{ {
_log.Debug("Creating Tag: {Tag}", tag); _log.Debug("Creating Tag: {Tag}", tag);
var newTag = await _tagApiService.CreateTag(tag); var newTag = await _tagApiService.CreateTag(config, tag);
sonarrTags.Add(newTag); sonarrTags.Add(newTag);
} }
} }
@ -243,18 +251,25 @@ public class ReleaseProfileUpdater : IReleaseProfileUpdater
profileToUpdate.Tags = tagIds; profileToUpdate.Tags = tagIds;
} }
private async Task UpdateExistingProfile(SonarrReleaseProfile profileToUpdate, ReleaseProfileData profile, private async Task UpdateExistingProfile(
IServiceConfiguration config,
SonarrReleaseProfile profileToUpdate,
ReleaseProfileData profile,
IReadOnlyCollection<int> tagIds) IReadOnlyCollection<int> tagIds)
{ {
_log.Debug("Update existing profile with id {ProfileId}", profileToUpdate.Id); _log.Debug("Update existing profile with id {ProfileId}", profileToUpdate.Id);
SetupProfileRequestObject(profileToUpdate, profile, tagIds); 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}; var newProfile = new SonarrReleaseProfile {Name = title, Enabled = true};
SetupProfileRequestObject(newProfile, profile, tagIds); 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; using Recyclarr.TrashLib.Services.Sonarr.Api.Objects;
namespace Recyclarr.TrashLib.Services.Sonarr.Api; namespace Recyclarr.TrashLib.Services.Sonarr.Api;
public interface ISonarrTagApiService public interface ISonarrTagApiService
{ {
Task<IList<SonarrTag>> GetTags(); Task<IList<SonarrTag>> GetTags(IServiceConfiguration config);
Task<SonarrTag> CreateTag(string tag); Task<SonarrTag> CreateTag(IServiceConfiguration config, string tag);
} }

@ -1,4 +1,5 @@
using Flurl.Http; using Flurl.Http;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Http; using Recyclarr.TrashLib.Http;
using Recyclarr.TrashLib.Services.Sonarr.Api.Objects; using Recyclarr.TrashLib.Services.Sonarr.Api.Objects;
@ -13,15 +14,15 @@ public class SonarrTagApiService : ISonarrTagApiService
_service = service; _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>>(); .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}) .PostJsonAsync(new {label = tag})
.ReceiveJson<SonarrTag>(); .ReceiveJson<SonarrTag>();
} }

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

@ -13,9 +13,9 @@ public class SonarrCapabilityEnforcer
_capabilityChecker = capabilityChecker; _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) if (capabilities is null)
{ {
throw new ServiceIncompatibilityException("Capabilities could not be obtained"); throw new ServiceIncompatibilityException("Capabilities could not be obtained");

@ -1,6 +1,8 @@
using Recyclarr.TrashLib.Config.Services;
namespace Recyclarr.TrashLib.Services.System; namespace Recyclarr.TrashLib.Services.System;
public interface IServiceInformation 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; using Recyclarr.TrashLib.Services.System.Dto;
namespace Recyclarr.TrashLib.Services.System; namespace Recyclarr.TrashLib.Services.System;
public interface ISystemApiService 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 Flurl.Http;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Http; using Recyclarr.TrashLib.Http;
using Recyclarr.TrashLib.Services.System.Dto;
namespace Recyclarr.TrashLib.Services.System; namespace Recyclarr.TrashLib.Services.System;
public class ServiceInformation : IServiceInformation public class ServiceInformation : IServiceInformation
{ {
private readonly ISystemApiService _api;
private readonly ILogger _log; private readonly ILogger _log;
public ServiceInformation(ISystemApiService api, ILogger log) public ServiceInformation(ISystemApiService api, ILogger log)
{ {
_api = api;
_log = log; _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; } public async Task<Version?> GetVersion(IServiceConfiguration config)
private void LogServiceInfo(SystemStatus status)
{ {
_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 Flurl.Http;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Http; using Recyclarr.TrashLib.Http;
using Recyclarr.TrashLib.Services.System.Dto; using Recyclarr.TrashLib.Services.System.Dto;
@ -13,9 +14,9 @@ public class SystemApiService : ISystemApiService
_service = service; _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>(); .GetJsonAsync<SystemStatus>();
} }
} }

@ -112,5 +112,6 @@
&amp;lt;/Language&amp;gt;&#xD; &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> &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: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/=radarr/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Sonarr/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary> <s:Boolean x:Key="/Default/UserDictionary/Words/=Sonarr/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
Loading…
Cancel
Save