diff --git a/src/Common/Common.csproj b/src/Common/Common.csproj index 5f38880a..25cebc40 100644 --- a/src/Common/Common.csproj +++ b/src/Common/Common.csproj @@ -1,8 +1,10 @@ - - + + + + diff --git a/src/Common/Extensions/RxExtensions.cs b/src/Common/Extensions/RxExtensions.cs new file mode 100644 index 00000000..21bd9248 --- /dev/null +++ b/src/Common/Extensions/RxExtensions.cs @@ -0,0 +1,49 @@ +using System; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Threading; +using Serilog; + +namespace Common.Extensions +{ + public static class RxExtensions + { + public static IObservable Spy(this IObservable source, ILogger log, string? opName = null) + { + opName ??= "IObservable"; + log.Debug("{OpName}: Observable obtained on Thread: {ThreadId}", + opName, + Thread.CurrentThread.ManagedThreadId); + + return Observable.Create(obs => + { + log.Debug("{OpName}: Subscribed to on Thread: {ThreadId}", + opName, + Thread.CurrentThread.ManagedThreadId); + + try + { + var subscription = source + .Do( + x => log.Debug("{OpName}: OnNext({Result}) on Thread: {ThreadId}", opName, x, + Thread.CurrentThread.ManagedThreadId), + ex => log.Debug("{OpName}: OnError({Result}) on Thread: {ThreadId}", opName, ex.Message, + Thread.CurrentThread.ManagedThreadId), + () => log.Debug("{OpName}: OnCompleted() on Thread: {ThreadId}", opName, + Thread.CurrentThread.ManagedThreadId)) + .Subscribe(obs); + return new CompositeDisposable( + subscription, + Disposable.Create(() => log.Debug( + "{OpName}: Cleaned up on Thread: {ThreadId}", + opName, + Thread.CurrentThread.ManagedThreadId))); + } + finally + { + log.Debug("{OpName}: Subscription completed", opName); + } + }); + } + } +} diff --git a/src/Trash/Command/RadarrCommand.cs b/src/Trash/Command/RadarrCommand.cs index 3664c095..076104d6 100644 --- a/src/Trash/Command/RadarrCommand.cs +++ b/src/Trash/Command/RadarrCommand.cs @@ -20,17 +20,19 @@ namespace Trash.Command { private readonly IConfigurationLoader _configLoader; private readonly Func _customFormatUpdaterFactory; + private readonly ILogger _log; private readonly Func _qualityUpdaterFactory; public RadarrCommand( - ILogger logger, + ILogger log, LoggingLevelSwitch loggingLevelSwitch, ILogJanitor logJanitor, IConfigurationLoader configLoader, Func qualityUpdaterFactory, Func customFormatUpdaterFactory) - : base(logger, loggingLevelSwitch, logJanitor) + : base(log, loggingLevelSwitch, logJanitor) { + _log = log; _configLoader = configLoader; _qualityUpdaterFactory = qualityUpdaterFactory; _customFormatUpdaterFactory = customFormatUpdaterFactory; @@ -58,7 +60,7 @@ namespace Trash.Command } catch (FlurlHttpException e) { - Log.Error(e, "HTTP error while communicating with Radarr"); + _log.Error(e, "HTTP error while communicating with Radarr"); ExitDueToFailure(); } } diff --git a/src/Trash/Command/SonarrCommand.cs b/src/Trash/Command/SonarrCommand.cs index 496a7d60..0082da0e 100644 --- a/src/Trash/Command/SonarrCommand.cs +++ b/src/Trash/Command/SonarrCommand.cs @@ -19,18 +19,20 @@ namespace Trash.Command public class SonarrCommand : ServiceCommand { private readonly IConfigurationLoader _configLoader; + private readonly ILogger _log; private readonly Func _profileUpdaterFactory; private readonly Func _qualityUpdaterFactory; public SonarrCommand( - ILogger logger, + ILogger log, LoggingLevelSwitch loggingLevelSwitch, ILogJanitor logJanitor, IConfigurationLoader configLoader, Func profileUpdaterFactory, Func qualityUpdaterFactory) - : base(logger, loggingLevelSwitch, logJanitor) + : base(log, loggingLevelSwitch, logJanitor) { + _log = log; _configLoader = configLoader; _profileUpdaterFactory = profileUpdaterFactory; _qualityUpdaterFactory = qualityUpdaterFactory; @@ -60,7 +62,7 @@ namespace Trash.Command } catch (FlurlHttpException e) { - Log.Error(e, "HTTP error while communicating with Sonarr"); + _log.Error(e, "HTTP error while communicating with Sonarr"); ExitDueToFailure(); } } diff --git a/src/TrashLib.Tests/Sonarr/Api/SonarrReleaseProfileCompatibilityHandlerTest.cs b/src/TrashLib.Tests/Sonarr/Api/SonarrReleaseProfileCompatibilityHandlerTest.cs index c76a3d2d..af4ff295 100644 --- a/src/TrashLib.Tests/Sonarr/Api/SonarrReleaseProfileCompatibilityHandlerTest.cs +++ b/src/TrashLib.Tests/Sonarr/Api/SonarrReleaseProfileCompatibilityHandlerTest.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Reactive.Linq; +using System.Threading.Tasks; using AutoMapper; using FluentAssertions; using Newtonsoft.Json; @@ -55,7 +57,6 @@ namespace TrashLib.Tests.Sonarr.Api var result = sut.CompatibleReleaseProfileForReceiving(JObject.Parse(ctx.SerializeJson(dataV1))); - _ = compat.DidNotReceive().ArraysNeededForReleaseProfileRequiredAndIgnored; result.Should().BeEquivalentTo(new SonarrReleaseProfile { Ignored = new List {"one", "two", "three"} @@ -73,38 +74,43 @@ namespace TrashLib.Tests.Sonarr.Api var result = sut.CompatibleReleaseProfileForReceiving(JObject.Parse(ctx.SerializeJson(dataV2))); - _ = compat.DidNotReceive().ArraysNeededForReleaseProfileRequiredAndIgnored; result.Should().BeEquivalentTo(dataV2); } [Test] - public void Send_v2_to_v1() + public async Task Send_v2_to_v1() { using var ctx = new TestContext(); var compat = Substitute.For(); - compat.ArraysNeededForReleaseProfileRequiredAndIgnored.Returns(false); + compat.Capabilities.Returns(new[] + { + new SonarrCapabilities {ArraysNeededForReleaseProfileRequiredAndIgnored = false} + }.ToObservable()); var data = new SonarrReleaseProfile {Ignored = new List {"one", "two", "three"}}; var sut = new SonarrReleaseProfileCompatibilityHandler(compat, ctx.Mapper); - var result = sut.CompatibleReleaseProfileForSending(data); + var result = await sut.CompatibleReleaseProfileForSendingAsync(data); result.Should().BeEquivalentTo(new SonarrReleaseProfileV1 {Ignored = "one,two,three"}); } [Test] - public void Send_v2_to_v2() + public async Task Send_v2_to_v2() { using var ctx = new TestContext(); var compat = Substitute.For(); - compat.ArraysNeededForReleaseProfileRequiredAndIgnored.Returns(true); + compat.Capabilities.Returns(new[] + { + new SonarrCapabilities {ArraysNeededForReleaseProfileRequiredAndIgnored = true} + }.ToObservable()); var data = new SonarrReleaseProfile {Ignored = new List {"one", "two", "three"}}; var sut = new SonarrReleaseProfileCompatibilityHandler(compat, ctx.Mapper); - var result = sut.CompatibleReleaseProfileForSending(data); + var result = await sut.CompatibleReleaseProfileForSendingAsync(data); result.Should().BeEquivalentTo(data); } diff --git a/src/TrashLib/Sonarr/Api/ISonarrReleaseProfileCompatibilityHandler.cs b/src/TrashLib/Sonarr/Api/ISonarrReleaseProfileCompatibilityHandler.cs index 18d8bf5a..00cb77c0 100644 --- a/src/TrashLib/Sonarr/Api/ISonarrReleaseProfileCompatibilityHandler.cs +++ b/src/TrashLib/Sonarr/Api/ISonarrReleaseProfileCompatibilityHandler.cs @@ -1,3 +1,4 @@ +using System.Threading.Tasks; using Newtonsoft.Json.Linq; using TrashLib.Sonarr.Api.Objects; @@ -5,7 +6,7 @@ namespace TrashLib.Sonarr.Api { public interface ISonarrReleaseProfileCompatibilityHandler { - object CompatibleReleaseProfileForSending(SonarrReleaseProfile profile); + Task CompatibleReleaseProfileForSendingAsync(SonarrReleaseProfile profile); SonarrReleaseProfile CompatibleReleaseProfileForReceiving(JObject profile); } } diff --git a/src/TrashLib/Sonarr/Api/SonarrApi.cs b/src/TrashLib/Sonarr/Api/SonarrApi.cs index babdc206..098e921d 100644 --- a/src/TrashLib/Sonarr/Api/SonarrApi.cs +++ b/src/TrashLib/Sonarr/Api/SonarrApi.cs @@ -54,16 +54,18 @@ namespace TrashLib.Sonarr.Api public async Task UpdateReleaseProfile(SonarrReleaseProfile profileToUpdate) { + var profileToSend = await _profileHandler.CompatibleReleaseProfileForSendingAsync(profileToUpdate); await BaseUrl() .AppendPathSegment($"releaseprofile/{profileToUpdate.Id}") - .PutJsonAsync(_profileHandler.CompatibleReleaseProfileForSending(profileToUpdate)); + .PutJsonAsync(profileToSend); } public async Task CreateReleaseProfile(SonarrReleaseProfile newProfile) { + var profileToSend = await _profileHandler.CompatibleReleaseProfileForSendingAsync(newProfile); var response = await BaseUrl() .AppendPathSegment("releaseprofile") - .PostJsonAsync(_profileHandler.CompatibleReleaseProfileForSending(newProfile)) + .PostJsonAsync(profileToSend) .ReceiveJson(); return _profileHandler.CompatibleReleaseProfileForReceiving(response); diff --git a/src/TrashLib/Sonarr/Api/SonarrReleaseProfileCompatibilityHandler.cs b/src/TrashLib/Sonarr/Api/SonarrReleaseProfileCompatibilityHandler.cs index 14e1c482..67961ff4 100644 --- a/src/TrashLib/Sonarr/Api/SonarrReleaseProfileCompatibilityHandler.cs +++ b/src/TrashLib/Sonarr/Api/SonarrReleaseProfileCompatibilityHandler.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; using System.IO; +using System.Reactive.Linq; +using System.Threading.Tasks; using AutoMapper; using Newtonsoft.Json.Linq; using Newtonsoft.Json.Schema; @@ -22,9 +24,10 @@ namespace TrashLib.Sonarr.Api _mapper = mapper; } - public object CompatibleReleaseProfileForSending(SonarrReleaseProfile profile) + public async Task CompatibleReleaseProfileForSendingAsync(SonarrReleaseProfile profile) { - return _compatibility.ArraysNeededForReleaseProfileRequiredAndIgnored + var capabilities = await _compatibility.Capabilities.LastAsync(); + return capabilities.ArraysNeededForReleaseProfileRequiredAndIgnored ? profile : _mapper.Map(profile); } diff --git a/src/TrashLib/Sonarr/ISonarrCompatibility.cs b/src/TrashLib/Sonarr/ISonarrCompatibility.cs index 7dd9b958..96f84a1f 100644 --- a/src/TrashLib/Sonarr/ISonarrCompatibility.cs +++ b/src/TrashLib/Sonarr/ISonarrCompatibility.cs @@ -1,10 +1,10 @@ +using System; + namespace TrashLib.Sonarr { public interface ISonarrCompatibility { - bool SupportsNamedReleaseProfiles { get; } - bool ArraysNeededForReleaseProfileRequiredAndIgnored { get; } - string InformationalVersion { get; } - string MinimumVersion { get; } + IObservable Capabilities { get; } + Version MinimumVersion { get; } } } diff --git a/src/TrashLib/Sonarr/ReleaseProfile/ReleaseProfileUpdater.cs b/src/TrashLib/Sonarr/ReleaseProfile/ReleaseProfileUpdater.cs index e6bd9bf5..0b88515d 100644 --- a/src/TrashLib/Sonarr/ReleaseProfile/ReleaseProfileUpdater.cs +++ b/src/TrashLib/Sonarr/ReleaseProfile/ReleaseProfileUpdater.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using System.Reactive.Linq; using System.Threading.Tasks; using Common.Extensions; using Serilog; @@ -53,12 +54,19 @@ namespace TrashLib.Sonarr.ReleaseProfile } } - private void DoVersionEnforcement() + private async Task DoVersionEnforcement() { - if (!_compatibility.SupportsNamedReleaseProfiles) + // _compatibility.Capabilities + // .Where(x => !x.SupportsNamedReleaseProfiles) + // .Subscribe(x => throw new VersionException( + // $"Your Sonarr version {x.Version} does not meet the minimum " + + // $"required version of {_compatibility.MinimumVersion} to use this program")); + + var capabilities = await _compatibility.Capabilities.LastAsync(); + if (!capabilities.SupportsNamedReleaseProfiles) { throw new VersionException( - $"Your Sonarr version {_compatibility.InformationalVersion} does not meet the minimum " + + $"Your Sonarr version {capabilities.Version} does not meet the minimum " + $"required version of {_compatibility.MinimumVersion} to use this program"); } } @@ -128,7 +136,7 @@ namespace TrashLib.Sonarr.ReleaseProfile private async Task ProcessReleaseProfiles(IDictionary profiles, ReleaseProfileConfig config) { - DoVersionEnforcement(); + await DoVersionEnforcement(); List tagIds = new(); diff --git a/src/TrashLib/Sonarr/SonarrCapabilities.cs b/src/TrashLib/Sonarr/SonarrCapabilities.cs new file mode 100644 index 00000000..e6b44b3e --- /dev/null +++ b/src/TrashLib/Sonarr/SonarrCapabilities.cs @@ -0,0 +1,27 @@ +using System; + +namespace TrashLib.Sonarr +{ + public record SonarrCapabilities + { + public SonarrCapabilities() + { + Version = new Version(); + } + + public SonarrCapabilities(Version version) + { + Version = version; + } + + public Version Version { get; } + + public bool SupportsNamedReleaseProfiles { get; init; } + + // Background: Issue #16 filed which points to a backward-breaking API + // change made in Sonarr at commit [deed85d2f]. + // + // [deed85d2f]: https://github.com/Sonarr/Sonarr/commit/deed85d2f9147e6180014507ef4f5af3695b0c61 + public bool ArraysNeededForReleaseProfileRequiredAndIgnored { get; init; } + } +} diff --git a/src/TrashLib/Sonarr/SonarrCompatibility.cs b/src/TrashLib/Sonarr/SonarrCompatibility.cs index 34523bad..1a3810c1 100644 --- a/src/TrashLib/Sonarr/SonarrCompatibility.cs +++ b/src/TrashLib/Sonarr/SonarrCompatibility.cs @@ -1,6 +1,6 @@ using System; +using System.Reactive.Concurrency; using System.Reactive.Linq; -using System.Reactive.Threading.Tasks; using Flurl.Http; using TrashLib.Config; @@ -8,30 +8,32 @@ namespace TrashLib.Sonarr { public class SonarrCompatibility : ISonarrCompatibility { - private Version _version = new(); - public SonarrCompatibility(IServerInfo serverInfo) { - var task = serverInfo.BuildRequest() - .AppendPathSegment("system/status") - .GetJsonAsync(); - - task.ToObservable() + Capabilities = Observable.FromAsync( + async () => await serverInfo.BuildRequest() + .AppendPathSegment("system/status") + .GetJsonAsync(), NewThreadScheduler.Default) + .Timeout(TimeSpan.FromSeconds(15)) .Select(x => new Version(x.version)) - .Subscribe(x => _version = x); + .Select(BuildCapabilitiesObject) + .Replay(1) + .AutoConnect(); } - public bool SupportsNamedReleaseProfiles => - _version >= new Version(MinimumVersion); + public IObservable Capabilities { get; } + public Version MinimumVersion => new("3.0.4.1098"); - // Background: Issue #16 filed which points to a backward-breaking API - // change made in Sonarr at commit [deed85d2f]. - // - // [deed85d2f]: https://github.com/Sonarr/Sonarr/commit/deed85d2f9147e6180014507ef4f5af3695b0c61 - public bool ArraysNeededForReleaseProfileRequiredAndIgnored => - _version >= new Version("3.0.6.1355"); + private SonarrCapabilities BuildCapabilitiesObject(Version version) + { + return new SonarrCapabilities(version) + { + SupportsNamedReleaseProfiles = + version >= MinimumVersion, - public string InformationalVersion => _version.ToString(); - public string MinimumVersion => "3.0.4.1098"; + ArraysNeededForReleaseProfileRequiredAndIgnored = + version >= new Version("3.0.6.1355") + }; + } } }