The Sonarr developers made a backward-breaking API change resulting in
Trash Updater being unable to obtain, create, or update release
profiles. This fix keeps backward compatibility with the previous and
current schema at the cost of additional code complexity.
The specific breakage was in the Ignored and Required properties of the
Release Profile JSON schema. They were converted from string type to
array.
Offending change:
deed85d2f9/src/NzbDrone.Core/Datastore/Migration/162_release_profile_to_array.cs
Fixes #16
pull/47/head
parent
b1651c89c6
commit
6f2d946902
@ -0,0 +1,112 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using AutoMapper;
|
||||
using FluentAssertions;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using NSubstitute;
|
||||
using NUnit.Framework;
|
||||
using TrashLib.Sonarr;
|
||||
using TrashLib.Sonarr.Api;
|
||||
using TrashLib.Sonarr.Api.Objects;
|
||||
using TrashLib.Startup;
|
||||
|
||||
namespace TrashLib.Tests.Sonarr.Api
|
||||
{
|
||||
[TestFixture]
|
||||
[Parallelizable(ParallelScope.All)]
|
||||
public class SonarrReleaseProfileCompatibilityHandlerTest
|
||||
{
|
||||
private class TestContext : IDisposable
|
||||
{
|
||||
private readonly JsonSerializerSettings _jsonSettings;
|
||||
|
||||
public TestContext()
|
||||
{
|
||||
_jsonSettings = new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver()
|
||||
};
|
||||
|
||||
Mapper = AutoMapperConfig.Setup();
|
||||
}
|
||||
|
||||
public IMapper Mapper { get; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
public string SerializeJson<T>(T obj)
|
||||
{
|
||||
return JsonConvert.SerializeObject(obj, _jsonSettings);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Receive_v1_to_v2()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
|
||||
var compat = Substitute.For<ISonarrCompatibility>();
|
||||
var dataV1 = new SonarrReleaseProfileV1 {Ignored = "one,two,three"};
|
||||
var sut = new SonarrReleaseProfileCompatibilityHandler(compat, ctx.Mapper);
|
||||
|
||||
var result = sut.CompatibleReleaseProfileForReceiving(JObject.Parse(ctx.SerializeJson(dataV1)));
|
||||
|
||||
_ = compat.DidNotReceive().ArraysNeededForReleaseProfileRequiredAndIgnored;
|
||||
result.Should().BeEquivalentTo(new SonarrReleaseProfile
|
||||
{
|
||||
Ignored = new List<string> {"one", "two", "three"}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Receive_v2_to_v2()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
|
||||
var compat = Substitute.For<ISonarrCompatibility>();
|
||||
var dataV2 = new SonarrReleaseProfile {Ignored = new List<string> {"one", "two", "three"}};
|
||||
var sut = new SonarrReleaseProfileCompatibilityHandler(compat, ctx.Mapper);
|
||||
|
||||
var result = sut.CompatibleReleaseProfileForReceiving(JObject.Parse(ctx.SerializeJson(dataV2)));
|
||||
|
||||
_ = compat.DidNotReceive().ArraysNeededForReleaseProfileRequiredAndIgnored;
|
||||
result.Should().BeEquivalentTo(dataV2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Send_v2_to_v1()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
|
||||
var compat = Substitute.For<ISonarrCompatibility>();
|
||||
compat.ArraysNeededForReleaseProfileRequiredAndIgnored.Returns(false);
|
||||
|
||||
var data = new SonarrReleaseProfile {Ignored = new List<string> {"one", "two", "three"}};
|
||||
var sut = new SonarrReleaseProfileCompatibilityHandler(compat, ctx.Mapper);
|
||||
|
||||
var result = sut.CompatibleReleaseProfileForSending(data);
|
||||
|
||||
result.Should().BeEquivalentTo(new SonarrReleaseProfileV1 {Ignored = "one,two,three"});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Send_v2_to_v2()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
|
||||
var compat = Substitute.For<ISonarrCompatibility>();
|
||||
compat.ArraysNeededForReleaseProfileRequiredAndIgnored.Returns(true);
|
||||
|
||||
var data = new SonarrReleaseProfile {Ignored = new List<string> {"one", "two", "three"}};
|
||||
var sut = new SonarrReleaseProfileCompatibilityHandler(compat, ctx.Mapper);
|
||||
|
||||
var result = sut.CompatibleReleaseProfileForSending(data);
|
||||
|
||||
result.Should().BeEquivalentTo(data);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using TrashLib.Sonarr.Api.Objects;
|
||||
|
||||
namespace TrashLib.Sonarr.Api
|
||||
{
|
||||
public interface ISonarrReleaseProfileCompatibilityHandler
|
||||
{
|
||||
object CompatibleReleaseProfileForSending(SonarrReleaseProfile profile);
|
||||
SonarrReleaseProfile CompatibleReleaseProfileForReceiving(JObject profile);
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using AutoMapper;
|
||||
using JetBrains.Annotations;
|
||||
using TrashLib.Sonarr.Api.Objects;
|
||||
|
||||
namespace TrashLib.Sonarr.Api.Mappings
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public class SonarrApiObjectMappingProfile : Profile
|
||||
{
|
||||
public SonarrApiObjectMappingProfile()
|
||||
{
|
||||
CreateMap<SonarrReleaseProfileV1, SonarrReleaseProfile>()
|
||||
.ForMember(d => d.Ignored, x => x.MapFrom(
|
||||
s => s.Ignored.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList()))
|
||||
.ForMember(d => d.Required, x => x.MapFrom(
|
||||
s => s.Required.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList()));
|
||||
|
||||
CreateMap<SonarrReleaseProfile, SonarrReleaseProfileV1>()
|
||||
.ForMember(d => d.Ignored, x => x.MapFrom(
|
||||
s => string.Join(',', s.Ignored)))
|
||||
.ForMember(d => d.Required, x => x.MapFrom(
|
||||
s => string.Join(',', s.Required)));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using AutoMapper;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json.Schema;
|
||||
using Newtonsoft.Json.Schema.Generation;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using Serilog;
|
||||
using TrashLib.Sonarr.Api.Objects;
|
||||
|
||||
namespace TrashLib.Sonarr.Api
|
||||
{
|
||||
public class SonarrReleaseProfileCompatibilityHandler : ISonarrReleaseProfileCompatibilityHandler
|
||||
{
|
||||
private readonly ISonarrCompatibility _compatibility;
|
||||
private readonly JSchemaGenerator _generator;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public SonarrReleaseProfileCompatibilityHandler(
|
||||
ISonarrCompatibility compatibility,
|
||||
IMapper mapper)
|
||||
{
|
||||
_compatibility = compatibility;
|
||||
_mapper = mapper;
|
||||
_generator = new JSchemaGenerator
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver(),
|
||||
DefaultRequired = Required.Default
|
||||
};
|
||||
}
|
||||
|
||||
public object CompatibleReleaseProfileForSending(SonarrReleaseProfile profile)
|
||||
{
|
||||
return _compatibility.ArraysNeededForReleaseProfileRequiredAndIgnored
|
||||
? profile
|
||||
: _mapper.Map<SonarrReleaseProfileV1>(profile);
|
||||
}
|
||||
|
||||
public SonarrReleaseProfile CompatibleReleaseProfileForReceiving(JObject profile)
|
||||
{
|
||||
JSchema? schema;
|
||||
IList<string>? errorMessages;
|
||||
|
||||
schema = _generator.Generate(typeof(SonarrReleaseProfile));
|
||||
if (profile.IsValid(schema, out errorMessages))
|
||||
{
|
||||
return profile.ToObject<SonarrReleaseProfile>()
|
||||
?? throw new InvalidDataException("SonarrReleaseProfile V2 parsing failed");
|
||||
}
|
||||
|
||||
Log.Debug("SonarrReleaseProfile is not a match for V2, proceeding to V1: {Reasons}", errorMessages);
|
||||
|
||||
schema = _generator.Generate(typeof(SonarrReleaseProfileV1));
|
||||
if (profile.IsValid(schema, out errorMessages))
|
||||
{
|
||||
// This will throw if there's an issue during mapping.
|
||||
return _mapper.Map<SonarrReleaseProfile>(profile.ToObject<SonarrReleaseProfileV1>());
|
||||
}
|
||||
|
||||
throw new InvalidDataException(
|
||||
$"SonarrReleaseProfile expected, but no supported schema detected: {errorMessages}");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
namespace TrashLib.Sonarr
|
||||
{
|
||||
public interface ISonarrCompatibility
|
||||
{
|
||||
bool SupportsNamedReleaseProfiles { get; }
|
||||
bool ArraysNeededForReleaseProfileRequiredAndIgnored { get; }
|
||||
string InformationalVersion { get; }
|
||||
string MinimumVersion { get; }
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Threading.Tasks;
|
||||
using Flurl;
|
||||
using Flurl.Http;
|
||||
using TrashLib.Config;
|
||||
|
||||
namespace TrashLib.Sonarr
|
||||
{
|
||||
public class SonarrCompatibility : ISonarrCompatibility
|
||||
{
|
||||
private Version _version = new();
|
||||
|
||||
public SonarrCompatibility(IServerInfo serverInfo)
|
||||
{
|
||||
var task = serverInfo.BuildUrl()
|
||||
.AppendPathSegment("system/status")
|
||||
.GetJsonAsync();
|
||||
|
||||
task.ToObservable()
|
||||
.Select(x => new Version(x.version))
|
||||
.Subscribe(x => _version = x);
|
||||
}
|
||||
|
||||
public bool SupportsNamedReleaseProfiles =>
|
||||
_version >= new Version(MinimumVersion);
|
||||
|
||||
// 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");
|
||||
|
||||
public string InformationalVersion => _version.ToString();
|
||||
public string MinimumVersion => "3.0.4.1098";
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
using AutoMapper;
|
||||
|
||||
namespace TrashLib.Startup
|
||||
{
|
||||
public static class AutoMapperConfig
|
||||
{
|
||||
public static IMapper Setup()
|
||||
{
|
||||
// todo: consider using AutoMapper.Contrib.Autofac.DependencyInjection
|
||||
var mapperConfig = new MapperConfiguration(cfg =>
|
||||
{
|
||||
cfg.AddMaps(typeof(AutoMapperConfig));
|
||||
});
|
||||
|
||||
#if DEBUG
|
||||
mapperConfig.AssertConfigurationIsValid();
|
||||
#endif
|
||||
|
||||
return mapperConfig.CreateMapper();
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in new issue