Your ROOT_URL in app.ini is https://git.cloudchain.link/ but you are visiting https://dash.bss.nz/open-source-mirrors/recyclarr/commit/77f15b7e7935835961cbdca1a9d76ea16103fd5a You should set ROOT_URL correctly, otherwise the web may not work correctly.

refactor: Reorganize custom format code to support reuse with Sonarr

pull/124/head
Robert Dailey 3 years ago
parent ef39403f32
commit 77f15b7e79

@ -0,0 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\Common\Common.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1,32 @@
using System.IO.Abstractions;
using System.IO.Abstractions.TestingHelpers;
using System.Reflection;
namespace Common.TestLibrary;
public static class CommonMockFileSystemExtensions
{
public static void AddFileFromResource(this MockFileSystem fs, string resourceFilename)
{
fs.AddFileFromResource(resourceFilename, resourceFilename, Assembly.GetCallingAssembly());
}
public static void AddFileFromResource(this MockFileSystem fs, IFileInfo file, string resourceFilename,
string resourceDir = "Data")
{
fs.AddFileFromResource(file.FullName, resourceFilename, Assembly.GetCallingAssembly(), resourceDir);
}
public static void AddFileFromResource(this MockFileSystem fs, string file, string resourceFilename,
string resourceDir = "Data")
{
fs.AddFileFromResource(file, resourceFilename, Assembly.GetCallingAssembly(), resourceDir);
}
public static void AddFileFromResource(this MockFileSystem fs, string file, string resourceFilename,
Assembly assembly, string resourceDir = "Data")
{
var resourceReader = new ResourceDataReader(assembly, resourceDir);
fs.AddFile(file, new MockFileData(resourceReader.ReadData(resourceFilename)));
}
}

@ -9,6 +9,12 @@ public class ResourceDataReader
private readonly string? _namespace;
private readonly string _subdirectory;
public ResourceDataReader(Assembly assembly, string subdirectory = "")
{
_subdirectory = subdirectory;
_assembly = assembly;
}
public ResourceDataReader(Type typeWithNamespaceToUse, string subdirectory = "")
{
_subdirectory = subdirectory;
@ -17,21 +23,48 @@ public class ResourceDataReader
}
public string ReadData(string filename)
{
var resourcePath = BuildResourceName(filename);
var foundResource = FindResourcePath(resourcePath);
return GetResourceData(foundResource);
}
private string BuildResourceName(string filename)
{
var nameBuilder = new StringBuilder();
nameBuilder.Append(_namespace);
if (!string.IsNullOrEmpty(_namespace))
{
nameBuilder.Append($"{_namespace}.");
}
if (!string.IsNullOrEmpty(_subdirectory))
{
nameBuilder.Append($".{_subdirectory}");
nameBuilder.Append($"{_subdirectory}.");
}
nameBuilder.Append($".{filename}");
nameBuilder.Append(filename);
return nameBuilder.ToString();
}
var resourceName = nameBuilder.ToString();
using var stream = _assembly?.GetManifestResourceStream(resourceName);
if (stream == null)
private string FindResourcePath(string resourcePath)
{
var foundResource = _assembly?.GetManifestResourceNames()
.FirstOrDefault(x => x.EndsWith(resourcePath));
if (foundResource is null)
{
throw new ArgumentException($"Embedded resource not found: {resourcePath}");
}
return foundResource;
}
private string GetResourceData(string resourcePath)
{
using var stream = _assembly?.GetManifestResourceStream(resourcePath);
if (stream is null)
{
throw new ArgumentException($"Embedded resource not found: {resourceName}");
throw new ArgumentException($"Unable to open embedded resource: {resourcePath}");
}
using var reader = new StreamReader(stream);

@ -37,10 +37,7 @@
<PackageReference Include="GitVersion.MsBuild" PrivateAssets="All" />
</ItemGroup>
<PropertyGroup Condition="$(ProjectName.EndsWith('.Tests'))">
</PropertyGroup>
<ItemGroup Condition="$(ProjectName.EndsWith('.Tests'))">
<ItemGroup Condition="$(ProjectName.EndsWith('.Tests')) Or $(ProjectName.EndsWith('.TestLibrary'))">
<PackageReference Include="AutofacContrib.NSubstitute" PrivateAssets="All" />
<PackageReference Include="AutoFixture" PrivateAssets="All" />
<PackageReference Include="AutoFixture.AutoNSubstitute" PrivateAssets="All" />
@ -61,7 +58,7 @@
<PackageReference Include="TestableIO.System.IO.Abstractions.Extensions" PrivateAssets="All" />
</ItemGroup>
<ItemGroup Condition="$(ProjectName.EndsWith('.Tests'))">
<ItemGroup Condition="$(ProjectName.EndsWith('.Tests')) Or $(ProjectName.EndsWith('.TestLibrary'))">
<EmbeddedResource Include="**\Data\*" />
</ItemGroup>
</Project>

@ -0,0 +1,13 @@
{
"json_paths": {
"radarr": {
"custom_formats": ["docs/json/radarr/cf"],
"qualities": ["docs/json/radarr/quality-size"]
},
"sonarr": {
"release_profiles": ["docs/json/sonarr/rp"],
"custom_formats": ["docs/json/sonarr/cf"],
"qualities": ["docs/json/sonarr/quality-size"]
}
}
}

@ -3,8 +3,13 @@ using System.IO.Abstractions.TestingHelpers;
using Autofac;
using Autofac.Features.ResolveAnything;
using CliFx.Infrastructure;
using Common.TestLibrary;
using NSubstitute;
using NUnit.Framework;
using Serilog.Events;
using TrashLib.Startup;
using VersionControl;
using VersionControl.Wrappers;
namespace Recyclarr.TestLibrary;
@ -12,20 +17,38 @@ namespace Recyclarr.TestLibrary;
public abstract class IntegrationFixture : IDisposable
{
private readonly ILifetimeScope _container;
private readonly FakeConsole _console = new();
protected IntegrationFixture()
{
var compRoot = new CompositionRoot();
_container = compRoot.Setup(default, _console, LogEventLevel.Debug).Container
_container = compRoot.Setup(default, Console, LogEventLevel.Debug).Container
.BeginLifetimeScope(builder =>
{
builder.RegisterSource<AnyConcreteTypeNotAlreadyRegisteredSource>();
builder.RegisterInstance(Fs).As<IFileSystem>();
RegisterMockFor<IGitRepository>(builder);
RegisterMockFor<IGitRepositoryFactory>(builder);
RegisterMockFor<IRepositoryStaticWrapper>(builder);
});
SetupMetadataJson();
}
private void SetupMetadataJson()
{
var paths = Resolve<IAppPaths>();
var metadataFile = paths.RepoDirectory.File("metadata.json");
Fs.AddFileFromResource(metadataFile, "metadata.json");
}
protected MockFileSystem Fs { get; } = new();
protected FakeInMemoryConsole Console { get; } = new();
private static void RegisterMockFor<T>(ContainerBuilder builder) where T : class
{
builder.RegisterInstance(Substitute.For<T>()).As<T>();
}
protected T Resolve<T>(Action<ContainerBuilder> customRegistrations) where T : notnull
{
@ -46,7 +69,7 @@ public abstract class IntegrationFixture : IDisposable
}
_container.Dispose();
_console.Dispose();
Console.Dispose();
}
public void Dispose()

@ -1,9 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\Common.TestLibrary\Common.TestLibrary.csproj" />
<ProjectReference Include="..\Recyclarr\Recyclarr.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="NUnit" PrivateAssets="All" />
<PackageReference Include="System.IO.Abstractions.TestingHelpers" />
</ItemGroup>
</Project>

@ -33,6 +33,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Recyclarr.TestLibrary", "Re
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Recyclarr.Gui", "Recyclarr.Gui\Recyclarr.Gui.csproj", "{53EECBC0-E0EA-4D6C-925C-5DB8C42CCB85}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common.TestLibrary", "Common.TestLibrary\Common.TestLibrary.csproj", "{A92321B5-2796-467B-B5A5-2BFC41167A25}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -91,6 +93,10 @@ Global
{53EECBC0-E0EA-4D6C-925C-5DB8C42CCB85}.Debug|Any CPU.Build.0 = Debug|Any CPU
{53EECBC0-E0EA-4D6C-925C-5DB8C42CCB85}.Release|Any CPU.ActiveCfg = Release|Any CPU
{53EECBC0-E0EA-4D6C-925C-5DB8C42CCB85}.Release|Any CPU.Build.0 = Release|Any CPU
{A92321B5-2796-467B-B5A5-2BFC41167A25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A92321B5-2796-467B-B5A5-2BFC41167A25}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A92321B5-2796-467B-B5A5-2BFC41167A25}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A92321B5-2796-467B-B5A5-2BFC41167A25}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
EndGlobalSection

@ -3,8 +3,9 @@ using JetBrains.Annotations;
using Recyclarr.Config;
using Serilog;
using TrashLib.Extensions;
using TrashLib.Services.CustomFormat;
using TrashLib.Services.Radarr;
using TrashLib.Services.Radarr.Config;
using TrashLib.Services.Radarr.CustomFormat;
using TrashLib.Services.Radarr.QualityDefinition;
namespace Recyclarr.Command;
@ -56,7 +57,7 @@ internal class RadarrCommand : ServiceCommand
if (config.CustomFormats.Count > 0)
{
await customFormatUpdaterFactory().Process(Preview, config);
await customFormatUpdaterFactory().Process(Preview, config.CustomFormats);
}
}
}

@ -4,6 +4,7 @@ using JetBrains.Annotations;
using Recyclarr.Config;
using Serilog;
using TrashLib.Extensions;
using TrashLib.Services.CustomFormat;
using TrashLib.Services.Sonarr;
using TrashLib.Services.Sonarr.Config;
using TrashLib.Services.Sonarr.QualityDefinition;
@ -30,6 +31,10 @@ public class SonarrCommand : ServiceCommand
"List available quality definition types from the guide.")]
public bool ListQualities { get; [UsedImplicitly] set; }
[CommandOption("list-custom-formats", Description =
"List available custom formats from the guide in YAML format.")]
public bool ListCustomFormats { get; [UsedImplicitly] set; }
public override string Name => "Sonarr";
public override async Task Process(IServiceLocatorProxy container)
@ -41,6 +46,7 @@ public class SonarrCommand : ServiceCommand
var qualityUpdaterFactory = container.Resolve<Func<ISonarrQualityDefinitionUpdater>>();
var configLoader = container.Resolve<IConfigurationLoader<SonarrConfiguration>>();
var log = container.Resolve<ILogger>();
var customFormatUpdaterFactory = container.Resolve<Func<ICustomFormatUpdater>>();
if (ListReleaseProfiles)
{
@ -54,6 +60,12 @@ public class SonarrCommand : ServiceCommand
return;
}
if (ListCustomFormats)
{
lister.ListCustomFormats();
return;
}
if (ListTerms != "empty")
{
if (!string.IsNullOrEmpty(ListTerms))
@ -82,6 +94,11 @@ public class SonarrCommand : ServiceCommand
{
await qualityUpdaterFactory().Process(Preview, config);
}
if (config.CustomFormats.Count > 0)
{
await customFormatUpdaterFactory().Process(Preview, config.CustomFormats);
}
}
}
}

@ -16,7 +16,10 @@ using Serilog.Events;
using TrashLib;
using TrashLib.Cache;
using TrashLib.Config;
using TrashLib.Config.Services;
using TrashLib.Repo;
using TrashLib.Services.Common;
using TrashLib.Services.CustomFormat;
using TrashLib.Services.Radarr;
using TrashLib.Services.Sonarr;
using TrashLib.Startup;
@ -45,12 +48,15 @@ public class CompositionRoot : ICompositionRoot
builder.RegisterModule<VersionControlAutofacModule>();
builder.RegisterModule<MigrationAutofacModule>();
builder.RegisterModule<RepoAutofacModule>();
builder.RegisterModule<CustomFormatAutofacModule>();
builder.RegisterModule<GuideServicesAutofacModule>();
// Needed for Autofac.Extras.Ordering
builder.RegisterSource<OrderedRegistrationSource>();
builder.RegisterModule<CacheAutofacModule>();
builder.RegisterType<CacheStoragePath>().As<ICacheStoragePath>();
builder.RegisterType<ServerInfo>().As<IServerInfo>();
builder.RegisterType<ProgressBar>();
ConfigurationRegistrations(builder);

@ -1,4 +1,4 @@
using TrashLib.Services.Radarr.CustomFormat.Models;
using TrashLib.Services.CustomFormat.Models;
namespace TrashLib.TestLibrary;

@ -1,6 +1,6 @@
using Newtonsoft.Json.Linq;
using TrashLib.Services.Radarr.CustomFormat.Models;
using TrashLib.Services.Radarr.CustomFormat.Models.Cache;
using TrashLib.Services.CustomFormat.Models;
using TrashLib.Services.CustomFormat.Models.Cache;
namespace TrashLib.TestLibrary;

@ -19,7 +19,7 @@ public class SettingsPersisterTest
[Frozen] IAppPaths paths,
SettingsProvider sut)
{
var settings = sut.Settings;
_ = sut.Settings;
fileSystem.AllFiles.Should().ContainSingle(paths.SettingsPath.FullName);
}

@ -5,12 +5,12 @@ using NSubstitute;
using NUnit.Framework;
using Serilog;
using TrashLib.Cache;
using TrashLib.Services.Radarr.CustomFormat;
using TrashLib.Services.Radarr.CustomFormat.Models;
using TrashLib.Services.Radarr.CustomFormat.Models.Cache;
using TrashLib.Services.Radarr.CustomFormat.Processors.PersistenceSteps;
using TrashLib.Services.CustomFormat;
using TrashLib.Services.CustomFormat.Models;
using TrashLib.Services.CustomFormat.Models.Cache;
using TrashLib.Services.CustomFormat.Processors.PersistenceSteps;
namespace TrashLib.Tests.Radarr.CustomFormat;
namespace TrashLib.Tests.CustomFormat;
[TestFixture]
[Parallelizable(ParallelScope.All)]

@ -4,10 +4,10 @@ using AutoFixture.NUnit3;
using FluentAssertions;
using NUnit.Framework;
using TestLibrary.AutoFixture;
using TrashLib.Services.Radarr.CustomFormat.Guide;
using TrashLib.Services.CustomFormat.Guide;
using TrashLib.Startup;
namespace TrashLib.Tests.Radarr.CustomFormat.Guide;
namespace TrashLib.Tests.CustomFormat.Guide;
[TestFixture]
[Parallelizable(ParallelScope.All)]

@ -0,0 +1,55 @@
using System.IO.Abstractions.Extensions;
using System.IO.Abstractions.TestingHelpers;
using FluentAssertions;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using Recyclarr.TestLibrary;
using TestLibrary.FluentAssertions;
using TrashLib.Services.CustomFormat.Guide;
using TrashLib.TestLibrary;
namespace TrashLib.Tests.CustomFormat.Guide;
[TestFixture]
[Parallelizable(ParallelScope.All)]
public class CustomFormatLoaderTest : IntegrationFixture
{
[Test]
public void Get_custom_format_json_works()
{
var sut = Resolve<ICustomFormatLoader>();
Fs.AddFile("first.json", new MockFileData("{'name':'first','trash_id':'1'}"));
Fs.AddFile("second.json", new MockFileData("{'name':'second','trash_id':'2'}"));
var results = sut.LoadAllCustomFormatsAtPaths(new[] {Fs.CurrentDirectory()});
results.Should().BeEquivalentTo(new[]
{
NewCf.Data("first", "1"),
NewCf.Data("second", "2")
});
}
[Test]
public void Trash_properties_are_removed()
{
Fs.AddFile("first.json", new MockFileData(@"
{
'name':'first',
'trash_id':'1',
'trash_foo': 'foo',
'trash_bar': 'bar',
'extra': 'e1'
}"));
var sut = Resolve<ICustomFormatLoader>();
var results = sut.LoadAllCustomFormatsAtPaths(new[] {Fs.CurrentDirectory()});
const string expectedExtraJson = @"{'name':'first','extra': 'e1'}";
results.Should()
.ContainSingle().Which.ExtraJson.Should()
.BeEquivalentTo(JObject.Parse(expectedExtraJson), op => op.Using(new JsonEquivalencyStep()));
}
}

@ -1,24 +1,21 @@
using AutoFixture.NUnit3;
using CliFx.Infrastructure;
using FluentAssertions;
using NSubstitute;
using NUnit.Framework;
using TestLibrary.AutoFixture;
using TrashLib.Services.Radarr.CustomFormat;
using TrashLib.Services.Radarr.CustomFormat.Guide;
using TrashLib.Services.Common;
using TrashLib.TestLibrary;
namespace TrashLib.Tests.Radarr.CustomFormat;
namespace TrashLib.Tests.CustomFormat;
[TestFixture]
[Parallelizable(ParallelScope.All)]
public class RadarrGuideDataListerTest
public class GuideDataListerTest
{
[Test, AutoMockData]
public void Custom_formats_appear_in_console_output(
[Frozen] IRadarrGuideService guide,
[Frozen(Matching.ImplementedInterfaces)] FakeInMemoryConsole console,
RadarrGuideDataLister sut)
GuideDataLister sut)
{
var testData = new[]
{
@ -26,9 +23,7 @@ public class RadarrGuideDataListerTest
NewCf.Data("Second", "456")
};
guide.GetCustomFormatData().Returns(testData);
sut.ListCustomFormats();
sut.ListCustomFormats(testData);
console.ReadOutputString().Should().ContainAll(
testData.SelectMany(x => new[] {x.Name, x.TrashId}));

@ -6,14 +6,15 @@ using NSubstitute;
using NUnit.Framework;
using Serilog;
using TestLibrary.FluentAssertions;
using TrashLib.Services.CustomFormat.Guide;
using TrashLib.Services.CustomFormat.Models;
using TrashLib.Services.CustomFormat.Processors;
using TrashLib.Services.CustomFormat.Processors.GuideSteps;
using TrashLib.Services.Radarr;
using TrashLib.Services.Radarr.Config;
using TrashLib.Services.Radarr.CustomFormat.Guide;
using TrashLib.Services.Radarr.CustomFormat.Models;
using TrashLib.Services.Radarr.CustomFormat.Processors;
using TrashLib.Services.Radarr.CustomFormat.Processors.GuideSteps;
using TrashLib.TestLibrary;
namespace TrashLib.Tests.Radarr.CustomFormat.Processors;
namespace TrashLib.Tests.CustomFormat.Processors;
[TestFixture]
[Parallelizable(ParallelScope.All)]
@ -35,8 +36,11 @@ public class GuideProcessorTest
public ResourceDataReader Data { get; }
public CustomFormatData ReadCustomFormat(string textFile) =>
LocalRepoRadarrGuideService.ParseCustomFormatData(ReadText(textFile));
public CustomFormatData ReadCustomFormat(string textFile)
{
var parser = new CustomFormatParser();
return parser.ParseCustomFormatData(ReadText(textFile));
}
public string ReadText(string textFile) => Data.ReadData(textFile);
public JObject ReadJson(string jsonFile) => JObject.Parse(ReadText(jsonFile));

@ -2,13 +2,13 @@ using FluentAssertions;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using TestLibrary.AutoFixture;
using TrashLib.Services.CustomFormat.Models;
using TrashLib.Services.CustomFormat.Models.Cache;
using TrashLib.Services.CustomFormat.Processors.GuideSteps;
using TrashLib.Services.Radarr.Config;
using TrashLib.Services.Radarr.CustomFormat.Models;
using TrashLib.Services.Radarr.CustomFormat.Models.Cache;
using TrashLib.Services.Radarr.CustomFormat.Processors.GuideSteps;
using TrashLib.TestLibrary;
namespace TrashLib.Tests.Radarr.CustomFormat.Processors.GuideSteps;
namespace TrashLib.Tests.CustomFormat.Processors.GuideSteps;
[TestFixture]
[Parallelizable(ParallelScope.All)]

@ -4,13 +4,13 @@ using Newtonsoft.Json.Linq;
using NUnit.Framework;
using TestLibrary.AutoFixture;
using TestLibrary.FluentAssertions;
using TrashLib.Services.CustomFormat.Models;
using TrashLib.Services.CustomFormat.Models.Cache;
using TrashLib.Services.CustomFormat.Processors.GuideSteps;
using TrashLib.Services.Radarr.Config;
using TrashLib.Services.Radarr.CustomFormat.Models;
using TrashLib.Services.Radarr.CustomFormat.Models.Cache;
using TrashLib.Services.Radarr.CustomFormat.Processors.GuideSteps;
using TrashLib.TestLibrary;
namespace TrashLib.Tests.Radarr.CustomFormat.Processors.GuideSteps;
namespace TrashLib.Tests.CustomFormat.Processors.GuideSteps;
[TestFixture]
[Parallelizable(ParallelScope.All)]

@ -1,11 +1,11 @@
using FluentAssertions;
using NUnit.Framework;
using TrashLib.Services.CustomFormat.Models;
using TrashLib.Services.CustomFormat.Processors.GuideSteps;
using TrashLib.Services.Radarr.Config;
using TrashLib.Services.Radarr.CustomFormat.Models;
using TrashLib.Services.Radarr.CustomFormat.Processors.GuideSteps;
using TrashLib.TestLibrary;
namespace TrashLib.Tests.Radarr.CustomFormat.Processors.GuideSteps;
namespace TrashLib.Tests.CustomFormat.Processors.GuideSteps;
[TestFixture]
[Parallelizable(ParallelScope.All)]

@ -3,13 +3,13 @@ using Newtonsoft.Json.Linq;
using NSubstitute;
using NUnit.Framework;
using TrashLib.Config.Services;
using TrashLib.Services.CustomFormat.Api;
using TrashLib.Services.CustomFormat.Models;
using TrashLib.Services.CustomFormat.Models.Cache;
using TrashLib.Services.CustomFormat.Processors;
using TrashLib.Services.Radarr.Config;
using TrashLib.Services.Radarr.CustomFormat.Api;
using TrashLib.Services.Radarr.CustomFormat.Models;
using TrashLib.Services.Radarr.CustomFormat.Models.Cache;
using TrashLib.Services.Radarr.CustomFormat.Processors;
namespace TrashLib.Tests.Radarr.CustomFormat.Processors;
namespace TrashLib.Tests.CustomFormat.Processors;
[TestFixture]
[Parallelizable(ParallelScope.All)]

@ -1,12 +1,12 @@
using NSubstitute;
using NUnit.Framework;
using TrashLib.Services.Radarr.CustomFormat.Api;
using TrashLib.Services.Radarr.CustomFormat.Models;
using TrashLib.Services.Radarr.CustomFormat.Models.Cache;
using TrashLib.Services.Radarr.CustomFormat.Processors.PersistenceSteps;
using TrashLib.Services.CustomFormat.Api;
using TrashLib.Services.CustomFormat.Models;
using TrashLib.Services.CustomFormat.Models.Cache;
using TrashLib.Services.CustomFormat.Processors.PersistenceSteps;
using TrashLib.TestLibrary;
namespace TrashLib.Tests.Radarr.CustomFormat.Processors.PersistenceSteps;
namespace TrashLib.Tests.CustomFormat.Processors.PersistenceSteps;
[TestFixture]
[Parallelizable(ParallelScope.All)]

@ -3,9 +3,9 @@ using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using TestLibrary.FluentAssertions;
using TrashLib.Services.Radarr.CustomFormat.Models;
using TrashLib.Services.Radarr.CustomFormat.Models.Cache;
using TrashLib.Services.Radarr.CustomFormat.Processors.PersistenceSteps;
using TrashLib.Services.CustomFormat.Models;
using TrashLib.Services.CustomFormat.Models.Cache;
using TrashLib.Services.CustomFormat.Processors.PersistenceSteps;
using TrashLib.TestLibrary;
/* Sample Custom Format response from Radarr API
@ -36,7 +36,7 @@ using TrashLib.TestLibrary;
}
*/
namespace TrashLib.Tests.Radarr.CustomFormat.Processors.PersistenceSteps;
namespace TrashLib.Tests.CustomFormat.Processors.PersistenceSteps;
[TestFixture]
[Parallelizable(ParallelScope.All)]

@ -5,13 +5,13 @@ using Newtonsoft.Json.Linq;
using NSubstitute;
using NUnit.Framework;
using TestLibrary.NSubstitute;
using TrashLib.Services.Radarr.CustomFormat.Api;
using TrashLib.Services.Radarr.CustomFormat.Models;
using TrashLib.Services.Radarr.CustomFormat.Models.Cache;
using TrashLib.Services.Radarr.CustomFormat.Processors.PersistenceSteps;
using TrashLib.Services.CustomFormat.Api;
using TrashLib.Services.CustomFormat.Models;
using TrashLib.Services.CustomFormat.Models.Cache;
using TrashLib.Services.CustomFormat.Processors.PersistenceSteps;
using TrashLib.TestLibrary;
namespace TrashLib.Tests.Radarr.CustomFormat.Processors.PersistenceSteps;
namespace TrashLib.Tests.CustomFormat.Processors.PersistenceSteps;
[TestFixture]
[Parallelizable(ParallelScope.All)]

@ -1,78 +0,0 @@
using System.IO.Abstractions;
using System.IO.Abstractions.TestingHelpers;
using AutoFixture.NUnit3;
using FluentAssertions;
using Newtonsoft.Json.Linq;
using NSubstitute;
using NUnit.Framework;
using TestLibrary.AutoFixture;
using TestLibrary.FluentAssertions;
using TrashLib.Repo;
using TrashLib.Services.Radarr.CustomFormat.Guide;
using TrashLib.Startup;
using TrashLib.TestLibrary;
namespace TrashLib.Tests.Radarr.CustomFormat.Guide;
[TestFixture]
[Parallelizable(ParallelScope.All)]
public class LocalRepoRadarrGuideServiceTest
{
[Test, AutoMockData]
public void Get_custom_format_json_works(
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
[Frozen] IAppPaths appPaths,
[Frozen] IRepoPaths repoPaths,
LocalRepoRadarrGuideService sut)
{
var jsonDir = appPaths.RepoDirectory
.SubDirectory("docs")
.SubDirectory("json")
.SubDirectory("radarr");
fs.AddFile(jsonDir.File("first.json").FullName, new MockFileData("{'name':'first','trash_id':'1'}"));
fs.AddFile(jsonDir.File("second.json").FullName, new MockFileData("{'name':'second','trash_id':'2'}"));
repoPaths.RadarrCustomFormatPaths.Returns(new[] {jsonDir});
var results = sut.GetCustomFormatData();
results.Should().BeEquivalentTo(new[]
{
NewCf.Data("first", "1"),
NewCf.Data("second", "2")
});
}
[Test, AutoMockData]
public void Trash_properties_are_removed(
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
[Frozen] IAppPaths appPaths,
[Frozen] IRepoPaths repoPaths,
LocalRepoRadarrGuideService sut)
{
var jsonDir = appPaths.RepoDirectory
.SubDirectory("docs")
.SubDirectory("json")
.SubDirectory("radarr");
fs.AddFile(jsonDir.File("first.json").FullName, new MockFileData(@"
{
'name':'first',
'trash_id':'1',
'trash_foo': 'foo',
'trash_bar': 'bar',
'extra': 'e1'
}"));
repoPaths.RadarrCustomFormatPaths.Returns(new[] {jsonDir});
var results = sut.GetCustomFormatData();
const string expectedExtraJson = @"{'name':'first','extra': 'e1'}";
results.Should()
.ContainSingle().Which.ExtraJson.Should()
.BeEquivalentTo(JObject.Parse(expectedExtraJson), op => op.Using(new JsonEquivalencyStep()));
}
}

@ -5,4 +5,5 @@ namespace TrashLib.Config.Services;
public interface IServerInfo
{
Url BuildRequest();
string SanitizedBaseUrl { get; }
}

@ -1,8 +1,9 @@
using Flurl;
using TrashLib.Extensions;
namespace TrashLib.Config.Services;
internal class ServerInfo : IServerInfo
public class ServerInfo : IServerInfo
{
private readonly IConfigurationProvider _config;
@ -20,4 +21,6 @@ internal class ServerInfo : IServerInfo
.AppendPathSegment("api/v3")
.SetQueryParams(new {apikey = apiKey});
}
public string SanitizedBaseUrl => FlurlLogging.SanitizeUrl(_config.ActiveConfiguration.BaseUrl);
}

@ -8,4 +8,5 @@ public interface IRepoPaths
IReadOnlyCollection<IDirectoryInfo> SonarrReleaseProfilePaths { get; }
IReadOnlyCollection<IDirectoryInfo> SonarrQualityPaths { get; }
IReadOnlyCollection<IDirectoryInfo> RadarrQualityPaths { get; }
IReadOnlyCollection<IDirectoryInfo> SonarrCustomFormatPaths { get; }
}

@ -7,7 +7,8 @@ public record RadarrMetadata(
public record SonarrMetadata(
IReadOnlyCollection<string> ReleaseProfiles,
IReadOnlyCollection<string> Qualities
IReadOnlyCollection<string> Qualities,
IReadOnlyCollection<string> CustomFormats
);
public record JsonPaths(

@ -6,5 +6,6 @@ public record RepoPaths(
IReadOnlyCollection<IDirectoryInfo> RadarrCustomFormatPaths,
IReadOnlyCollection<IDirectoryInfo> SonarrReleaseProfilePaths,
IReadOnlyCollection<IDirectoryInfo> RadarrQualityPaths,
IReadOnlyCollection<IDirectoryInfo> SonarrQualityPaths
IReadOnlyCollection<IDirectoryInfo> SonarrQualityPaths,
IReadOnlyCollection<IDirectoryInfo> SonarrCustomFormatPaths
) : IRepoPaths;

@ -29,7 +29,8 @@ public class RepoPathsFactory : IRepoPathsFactory
ToDirectoryInfoList(metadata.JsonPaths.Radarr.CustomFormats),
ToDirectoryInfoList(metadata.JsonPaths.Sonarr.ReleaseProfiles),
ToDirectoryInfoList(metadata.JsonPaths.Radarr.Qualities),
ToDirectoryInfoList(metadata.JsonPaths.Sonarr.Qualities)
ToDirectoryInfoList(metadata.JsonPaths.Sonarr.Qualities),
ToDirectoryInfoList(metadata.JsonPaths.Sonarr.CustomFormats)
);
}
}

@ -0,0 +1,28 @@
using CliFx.Infrastructure;
using TrashLib.Services.CustomFormat.Models;
namespace TrashLib.Services.Common;
public class GuideDataLister : IGuideDataLister
{
private readonly IConsole _console;
public GuideDataLister(IConsole console)
{
_console = console;
}
public void ListCustomFormats(IEnumerable<CustomFormatData> customFormats)
{
_console.Output.WriteLine("\nList of Custom Formats in the TRaSH Guides:\n");
foreach (var cf in customFormats)
{
_console.Output.WriteLine($" - {cf.TrashId} # {cf.Name}");
}
_console.Output.WriteLine(
"\nThe above Custom Formats are in YAML format and ready to be copied & pasted " +
"under the `trash_ids:` property.");
}
}

@ -0,0 +1,12 @@
using Autofac;
namespace TrashLib.Services.Common;
public class GuideServicesAutofacModule : Module
{
protected override void Load(ContainerBuilder builder)
{
base.Load(builder);
builder.RegisterType<GuideDataLister>().As<IGuideDataLister>();
}
}

@ -0,0 +1,8 @@
using TrashLib.Services.CustomFormat.Models;
namespace TrashLib.Services.Common;
public interface IGuideDataLister
{
void ListCustomFormats(IEnumerable<CustomFormatData> customFormats);
}

@ -2,9 +2,9 @@ using Flurl;
using Flurl.Http;
using Newtonsoft.Json.Linq;
using TrashLib.Config.Services;
using TrashLib.Services.Radarr.CustomFormat.Models;
using TrashLib.Services.CustomFormat.Models;
namespace TrashLib.Services.Radarr.CustomFormat.Api;
namespace TrashLib.Services.CustomFormat.Api;
internal class CustomFormatService : ICustomFormatService
{

@ -1,7 +1,7 @@
using Newtonsoft.Json.Linq;
using TrashLib.Services.Radarr.CustomFormat.Models;
using TrashLib.Services.CustomFormat.Models;
namespace TrashLib.Services.Radarr.CustomFormat.Api;
namespace TrashLib.Services.CustomFormat.Api;
public interface ICustomFormatService
{

@ -1,6 +1,6 @@
using Newtonsoft.Json.Linq;
namespace TrashLib.Services.Radarr.CustomFormat.Api;
namespace TrashLib.Services.CustomFormat.Api;
public interface IQualityProfileService
{

@ -3,7 +3,7 @@ using Flurl.Http;
using Newtonsoft.Json.Linq;
using TrashLib.Config.Services;
namespace TrashLib.Services.Radarr.CustomFormat.Api;
namespace TrashLib.Services.CustomFormat.Api;
internal class QualityProfileService : IQualityProfileService
{

@ -1,10 +1,10 @@
using Common.Extensions;
using Serilog;
using TrashLib.Cache;
using TrashLib.Services.Radarr.CustomFormat.Models;
using TrashLib.Services.Radarr.CustomFormat.Models.Cache;
using TrashLib.Services.CustomFormat.Models;
using TrashLib.Services.CustomFormat.Models.Cache;
namespace TrashLib.Services.Radarr.CustomFormat;
namespace TrashLib.Services.CustomFormat;
internal class CachePersister : ICachePersister
{

@ -0,0 +1,34 @@
using Autofac;
using Autofac.Extras.AggregateService;
using TrashLib.Services.CustomFormat.Api;
using TrashLib.Services.CustomFormat.Guide;
using TrashLib.Services.CustomFormat.Processors;
using TrashLib.Services.CustomFormat.Processors.GuideSteps;
using TrashLib.Services.CustomFormat.Processors.PersistenceSteps;
namespace TrashLib.Services.CustomFormat;
public class CustomFormatAutofacModule : Module
{
protected override void Load(ContainerBuilder builder)
{
builder.RegisterType<CustomFormatService>().As<ICustomFormatService>();
builder.RegisterType<QualityProfileService>().As<IQualityProfileService>();
builder.RegisterType<CustomFormatUpdater>().As<ICustomFormatUpdater>();
builder.RegisterType<CachePersister>().As<ICachePersister>();
builder.RegisterType<GuideProcessor>().As<IGuideProcessor>();
builder.RegisterType<CustomFormatLoader>().As<ICustomFormatLoader>();
builder.RegisterType<CustomFormatParser>().As<ICustomFormatParser>();
builder.RegisterAggregateService<IGuideProcessorSteps>();
builder.RegisterType<CustomFormatStep>().As<ICustomFormatStep>();
builder.RegisterType<ConfigStep>().As<IConfigStep>();
builder.RegisterType<QualityProfileStep>().As<IQualityProfileStep>();
builder.RegisterType<PersistenceProcessor>().As<IPersistenceProcessor>();
builder.RegisterAggregateService<IPersistenceProcessorSteps>();
builder.RegisterType<JsonTransactionStep>().As<IJsonTransactionStep>();
builder.RegisterType<CustomFormatApiPersistenceStep>().As<ICustomFormatApiPersistenceStep>();
builder.RegisterType<QualityProfileApiPersistenceStep>().As<IQualityProfileApiPersistenceStep>();
}
}

@ -1,12 +1,12 @@
using CliFx.Infrastructure;
using Common.Extensions;
using Serilog;
using TrashLib.Extensions;
using TrashLib.Config.Services;
using TrashLib.Services.CustomFormat.Processors;
using TrashLib.Services.CustomFormat.Processors.PersistenceSteps;
using TrashLib.Services.Radarr.Config;
using TrashLib.Services.Radarr.CustomFormat.Processors;
using TrashLib.Services.Radarr.CustomFormat.Processors.PersistenceSteps;
namespace TrashLib.Services.Radarr.CustomFormat;
namespace TrashLib.Services.CustomFormat;
internal class CustomFormatUpdater : ICustomFormatUpdater
{
@ -14,30 +14,33 @@ internal class CustomFormatUpdater : ICustomFormatUpdater
private readonly IGuideProcessor _guideProcessor;
private readonly IPersistenceProcessor _persistenceProcessor;
private readonly IConsole _console;
private readonly IServerInfo _serverInfo;
public CustomFormatUpdater(
ILogger log,
ICachePersister cache,
IGuideProcessor guideProcessor,
IPersistenceProcessor persistenceProcessor,
IConsole console)
IConsole console,
IServerInfo serverInfo)
{
Log = log;
_cache = cache;
_guideProcessor = guideProcessor;
_persistenceProcessor = persistenceProcessor;
_console = console;
_serverInfo = serverInfo;
}
private ILogger Log { get; }
public async Task Process(bool isPreview, RadarrConfiguration config)
public async Task Process(bool isPreview, IEnumerable<CustomFormatConfig> customFormats)
{
_cache.Load();
await _guideProcessor.BuildGuideDataAsync(config.CustomFormats.AsReadOnly(), _cache.CfCache);
await _guideProcessor.BuildGuideDataAsync(customFormats, _cache.CfCache);
if (!ValidateGuideDataAndCheckShouldProceed(config))
if (!ValidateGuideDataAndCheckShouldProceed())
{
return;
}
@ -135,7 +138,7 @@ internal class CustomFormatUpdater : ICustomFormatUpdater
}
}
private bool ValidateGuideDataAndCheckShouldProceed(RadarrConfiguration config)
private bool ValidateGuideDataAndCheckShouldProceed()
{
_console.Output.WriteLine("");
@ -186,7 +189,7 @@ internal class CustomFormatUpdater : ICustomFormatUpdater
if (_guideProcessor.ConfigData.Count == 0)
{
Log.Error("Guide processing yielded no custom formats for configured instance host {BaseUrl}",
FlurlLogging.SanitizeUrl(config.BaseUrl));
_serverInfo.SanitizedBaseUrl);
return false;
}

@ -4,7 +4,7 @@ using System.Text.RegularExpressions;
using Common.Extensions;
using TrashLib.Startup;
namespace TrashLib.Services.Radarr.CustomFormat.Guide;
namespace TrashLib.Services.CustomFormat.Guide;
public record CustomFormatGroupItem(string Name, string Anchor);

@ -0,0 +1,44 @@
using System.IO.Abstractions;
using System.Reactive.Linq;
using System.Reactive.Threading.Tasks;
using Common.Extensions;
using Newtonsoft.Json;
using Serilog;
using TrashLib.Services.CustomFormat.Models;
namespace TrashLib.Services.CustomFormat.Guide;
public class CustomFormatLoader : ICustomFormatLoader
{
private readonly ILogger _log;
private readonly ICustomFormatParser _parser;
public CustomFormatLoader(ILogger log, ICustomFormatParser parser)
{
_log = log;
_parser = parser;
}
public ICollection<CustomFormatData> LoadAllCustomFormatsAtPaths(IEnumerable<IDirectoryInfo> jsonPaths)
{
var jsonFiles = jsonPaths.SelectMany(x => x.GetFiles("*.json"));
return jsonFiles.ToObservable()
.Select(x => Observable.Defer(() => LoadJsonFromFile(x)))
.Merge(8)
.NotNull()
.ToEnumerable()
.ToList();
}
private IObservable<CustomFormatData?> LoadJsonFromFile(IFileInfo file)
{
return Observable.Using(file.OpenText, x => x.ReadToEndAsync().ToObservable())
.Do(_ => _log.Debug("Parsing CF Json: {Name}", file.Name))
.Select(_parser.ParseCustomFormatData)
.Catch((JsonException e) =>
{
_log.Warning("Failed to parse JSON file: {File} ({Reason})", file.Name, e.Message);
return Observable.Empty<CustomFormatData>();
});
}
}

@ -0,0 +1,34 @@
using System.Text.RegularExpressions;
using Common.Extensions;
using Newtonsoft.Json.Linq;
using TrashLib.Services.CustomFormat.Models;
namespace TrashLib.Services.CustomFormat.Guide;
public class CustomFormatParser : ICustomFormatParser
{
public CustomFormatData ParseCustomFormatData(string guideData)
{
var obj = JObject.Parse(guideData);
var name = obj.ValueOrThrow<string>("name");
var trashId = obj.ValueOrThrow<string>("trash_id");
int? finalScore = null;
if (obj.TryGetValue("trash_score", out var score))
{
finalScore = (int) score;
}
// Remove any properties starting with "trash_". Those are metadata that are not meant for Radarr itself Radarr
// supposedly drops this anyway, but I prefer it to be removed. ToList() is important here since removing the
// property itself modifies the collection, and we don't want the collection to get modified while still looping
// over it.
foreach (var trashProperty in obj.Properties().Where(x => Regex.IsMatch(x.Name, @"^trash_")).ToList())
{
trashProperty.Remove();
}
return new CustomFormatData(name, trashId, finalScore, obj);
}
}

@ -0,0 +1,9 @@
using System.IO.Abstractions;
using TrashLib.Services.CustomFormat.Models;
namespace TrashLib.Services.CustomFormat.Guide;
public interface ICustomFormatLoader
{
ICollection<CustomFormatData> LoadAllCustomFormatsAtPaths(IEnumerable<IDirectoryInfo> jsonPaths);
}

@ -0,0 +1,8 @@
using TrashLib.Services.CustomFormat.Models;
namespace TrashLib.Services.CustomFormat.Guide;
public interface ICustomFormatParser
{
CustomFormatData ParseCustomFormatData(string guideData);
}

@ -1,7 +1,7 @@
using TrashLib.Services.Radarr.CustomFormat.Models;
using TrashLib.Services.Radarr.CustomFormat.Models.Cache;
using TrashLib.Services.CustomFormat.Models;
using TrashLib.Services.CustomFormat.Models.Cache;
namespace TrashLib.Services.Radarr.CustomFormat;
namespace TrashLib.Services.CustomFormat;
public interface ICachePersister
{

@ -0,0 +1,8 @@
using TrashLib.Services.Radarr.Config;
namespace TrashLib.Services.CustomFormat;
public interface ICustomFormatUpdater
{
Task Process(bool isPreview, IEnumerable<CustomFormatConfig> config);
}

@ -1,7 +1,7 @@
using System.Collections.ObjectModel;
using TrashLib.Cache;
namespace TrashLib.Services.Radarr.CustomFormat.Models.Cache;
namespace TrashLib.Services.CustomFormat.Models.Cache;
[CacheObjectName("custom-format-cache")]
public class CustomFormatCache

@ -1,7 +1,7 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace TrashLib.Services.Radarr.CustomFormat.Models;
namespace TrashLib.Services.CustomFormat.Models;
public record CustomFormatData(
string Name,

@ -1,6 +1,6 @@
using TrashLib.Services.Radarr.Config;
namespace TrashLib.Services.Radarr.CustomFormat.Models;
namespace TrashLib.Services.CustomFormat.Models;
public class ProcessedConfigData
{

@ -1,8 +1,8 @@
using System.Diagnostics.CodeAnalysis;
using Newtonsoft.Json.Linq;
using TrashLib.Services.Radarr.CustomFormat.Models.Cache;
using TrashLib.Services.CustomFormat.Models.Cache;
namespace TrashLib.Services.Radarr.CustomFormat.Models;
namespace TrashLib.Services.CustomFormat.Models;
public class ProcessedCustomFormatData
{

@ -1,4 +1,4 @@
namespace TrashLib.Services.Radarr.CustomFormat.Models;
namespace TrashLib.Services.CustomFormat.Models;
public record FormatMappingEntry(ProcessedCustomFormatData CustomFormat, int Score);

@ -1,4 +1,4 @@
namespace TrashLib.Services.Radarr.CustomFormat.Models;
namespace TrashLib.Services.CustomFormat.Models;
public enum FormatScoreUpdateReason
{

@ -1,10 +1,10 @@
using TrashLib.Services.CustomFormat.Models;
using TrashLib.Services.CustomFormat.Models.Cache;
using TrashLib.Services.CustomFormat.Processors.GuideSteps;
using TrashLib.Services.Radarr;
using TrashLib.Services.Radarr.Config;
using TrashLib.Services.Radarr.CustomFormat.Guide;
using TrashLib.Services.Radarr.CustomFormat.Models;
using TrashLib.Services.Radarr.CustomFormat.Models.Cache;
using TrashLib.Services.Radarr.CustomFormat.Processors.GuideSteps;
namespace TrashLib.Services.Radarr.CustomFormat.Processors;
namespace TrashLib.Services.CustomFormat.Processors;
public interface IGuideProcessorSteps
{
@ -51,16 +51,18 @@ internal class GuideProcessor : IGuideProcessor
public IDictionary<string, List<ProcessedCustomFormatData>> DuplicatedCustomFormats
=> _steps.CustomFormat.DuplicatedCustomFormats;
public Task BuildGuideDataAsync(IReadOnlyCollection<CustomFormatConfig> config, CustomFormatCache? cache)
public Task BuildGuideDataAsync(IEnumerable<CustomFormatConfig> config, CustomFormatCache? cache)
{
if (_guideCustomFormatJson == null)
{
_guideCustomFormatJson = _guideService.GetCustomFormatData().ToList();
}
var listOfConfigs = config.ToList();
// Step 1: Process and filter the custom formats from the guide.
// Custom formats in the guide not mentioned in the config are filtered out.
_steps.CustomFormat.Process(_guideCustomFormatJson, config, cache);
_steps.CustomFormat.Process(_guideCustomFormatJson, listOfConfigs, cache);
// todo: Process cache entries that do not exist in the guide. Those should be deleted
// This might get taken care of when we rebuild the cache based on what is actually updated when
@ -69,7 +71,7 @@ internal class GuideProcessor : IGuideProcessor
// Step 2: Use the processed custom formats from step 1 to process the configuration.
// CFs in config not in the guide are filtered out.
// Actual CF objects are associated to the quality profile objects to reduce lookups
_steps.Config.Process(_steps.CustomFormat.ProcessedCustomFormats, config);
_steps.Config.Process(_steps.CustomFormat.ProcessedCustomFormats, listOfConfigs);
// Step 3: Use the processed config (which contains processed CFs) to process the quality profile scores.
// Score precedence logic is utilized here to decide the CF score per profile (same CF can actually have

@ -1,9 +1,9 @@
using Common.Extensions;
using Serilog;
using TrashLib.Services.CustomFormat.Models;
using TrashLib.Services.Radarr.Config;
using TrashLib.Services.Radarr.CustomFormat.Models;
namespace TrashLib.Services.Radarr.CustomFormat.Processors.GuideSteps;
namespace TrashLib.Services.CustomFormat.Processors.GuideSteps;
public class ConfigStep : IConfigStep
{

@ -1,9 +1,9 @@
using Common.Extensions;
using TrashLib.Services.CustomFormat.Models;
using TrashLib.Services.CustomFormat.Models.Cache;
using TrashLib.Services.Radarr.Config;
using TrashLib.Services.Radarr.CustomFormat.Models;
using TrashLib.Services.Radarr.CustomFormat.Models.Cache;
namespace TrashLib.Services.Radarr.CustomFormat.Processors.GuideSteps;
namespace TrashLib.Services.CustomFormat.Processors.GuideSteps;
public class CustomFormatStep : ICustomFormatStep
{

@ -1,7 +1,7 @@
using TrashLib.Services.CustomFormat.Models;
using TrashLib.Services.Radarr.Config;
using TrashLib.Services.Radarr.CustomFormat.Models;
namespace TrashLib.Services.Radarr.CustomFormat.Processors.GuideSteps;
namespace TrashLib.Services.CustomFormat.Processors.GuideSteps;
public interface IConfigStep
{

@ -1,8 +1,8 @@
using TrashLib.Services.CustomFormat.Models;
using TrashLib.Services.CustomFormat.Models.Cache;
using TrashLib.Services.Radarr.Config;
using TrashLib.Services.Radarr.CustomFormat.Models;
using TrashLib.Services.Radarr.CustomFormat.Models.Cache;
namespace TrashLib.Services.Radarr.CustomFormat.Processors.GuideSteps;
namespace TrashLib.Services.CustomFormat.Processors.GuideSteps;
public interface ICustomFormatStep
{

@ -1,6 +1,6 @@
using TrashLib.Services.Radarr.CustomFormat.Models;
using TrashLib.Services.CustomFormat.Models;
namespace TrashLib.Services.Radarr.CustomFormat.Processors.GuideSteps;
namespace TrashLib.Services.CustomFormat.Processors.GuideSteps;
public interface IQualityProfileStep
{

@ -1,6 +1,6 @@
using TrashLib.Services.Radarr.CustomFormat.Models;
using TrashLib.Services.CustomFormat.Models;
namespace TrashLib.Services.Radarr.CustomFormat.Processors.GuideSteps;
namespace TrashLib.Services.CustomFormat.Processors.GuideSteps;
internal class QualityProfileStep : IQualityProfileStep
{

@ -1,8 +1,8 @@
using TrashLib.Services.CustomFormat.Models;
using TrashLib.Services.CustomFormat.Models.Cache;
using TrashLib.Services.Radarr.Config;
using TrashLib.Services.Radarr.CustomFormat.Models;
using TrashLib.Services.Radarr.CustomFormat.Models.Cache;
namespace TrashLib.Services.Radarr.CustomFormat.Processors;
namespace TrashLib.Services.CustomFormat.Processors;
internal interface IGuideProcessor
{
@ -15,6 +15,6 @@ internal interface IGuideProcessor
IReadOnlyCollection<(string, string)> CustomFormatsWithOutdatedNames { get; }
IDictionary<string, List<ProcessedCustomFormatData>> DuplicatedCustomFormats { get; }
Task BuildGuideDataAsync(IReadOnlyCollection<CustomFormatConfig> config, CustomFormatCache? cache);
Task BuildGuideDataAsync(IEnumerable<CustomFormatConfig> config, CustomFormatCache? cache);
void Reset();
}

@ -1,8 +1,8 @@
using TrashLib.Services.Radarr.CustomFormat.Models;
using TrashLib.Services.Radarr.CustomFormat.Models.Cache;
using TrashLib.Services.Radarr.CustomFormat.Processors.PersistenceSteps;
using TrashLib.Services.CustomFormat.Models;
using TrashLib.Services.CustomFormat.Models.Cache;
using TrashLib.Services.CustomFormat.Processors.PersistenceSteps;
namespace TrashLib.Services.Radarr.CustomFormat.Processors;
namespace TrashLib.Services.CustomFormat.Processors;
public interface IPersistenceProcessor
{

@ -1,11 +1,11 @@
using TrashLib.Config.Services;
using TrashLib.Services.CustomFormat.Api;
using TrashLib.Services.CustomFormat.Models;
using TrashLib.Services.CustomFormat.Models.Cache;
using TrashLib.Services.CustomFormat.Processors.PersistenceSteps;
using TrashLib.Services.Radarr.Config;
using TrashLib.Services.Radarr.CustomFormat.Api;
using TrashLib.Services.Radarr.CustomFormat.Models;
using TrashLib.Services.Radarr.CustomFormat.Models.Cache;
using TrashLib.Services.Radarr.CustomFormat.Processors.PersistenceSteps;
namespace TrashLib.Services.Radarr.CustomFormat.Processors;
namespace TrashLib.Services.CustomFormat.Processors;
public interface IPersistenceProcessorSteps
{

@ -1,6 +1,6 @@
using TrashLib.Services.Radarr.CustomFormat.Api;
using TrashLib.Services.CustomFormat.Api;
namespace TrashLib.Services.Radarr.CustomFormat.Processors.PersistenceSteps;
namespace TrashLib.Services.CustomFormat.Processors.PersistenceSteps;
internal class CustomFormatApiPersistenceStep : ICustomFormatApiPersistenceStep
{

@ -1,6 +1,6 @@
using TrashLib.Services.Radarr.CustomFormat.Api;
using TrashLib.Services.CustomFormat.Api;
namespace TrashLib.Services.Radarr.CustomFormat.Processors.PersistenceSteps;
namespace TrashLib.Services.CustomFormat.Processors.PersistenceSteps;
public interface ICustomFormatApiPersistenceStep
{

@ -1,8 +1,8 @@
using Newtonsoft.Json.Linq;
using TrashLib.Services.Radarr.CustomFormat.Models;
using TrashLib.Services.Radarr.CustomFormat.Models.Cache;
using TrashLib.Services.CustomFormat.Models;
using TrashLib.Services.CustomFormat.Models.Cache;
namespace TrashLib.Services.Radarr.CustomFormat.Processors.PersistenceSteps;
namespace TrashLib.Services.CustomFormat.Processors.PersistenceSteps;
public interface IJsonTransactionStep
{

@ -1,7 +1,7 @@
using TrashLib.Services.Radarr.CustomFormat.Api;
using TrashLib.Services.Radarr.CustomFormat.Models;
using TrashLib.Services.CustomFormat.Api;
using TrashLib.Services.CustomFormat.Models;
namespace TrashLib.Services.Radarr.CustomFormat.Processors.PersistenceSteps;
namespace TrashLib.Services.CustomFormat.Processors.PersistenceSteps;
public interface IQualityProfileApiPersistenceStep
{

@ -1,10 +1,10 @@
using System.Collections.ObjectModel;
using Common.Extensions;
using Newtonsoft.Json.Linq;
using TrashLib.Services.Radarr.CustomFormat.Models;
using TrashLib.Services.Radarr.CustomFormat.Models.Cache;
using TrashLib.Services.CustomFormat.Models;
using TrashLib.Services.CustomFormat.Models.Cache;
namespace TrashLib.Services.Radarr.CustomFormat.Processors.PersistenceSteps;
namespace TrashLib.Services.CustomFormat.Processors.PersistenceSteps;
public class CustomFormatTransactionData
{

@ -1,9 +1,9 @@
using Common.Extensions;
using Newtonsoft.Json.Linq;
using TrashLib.Services.Radarr.CustomFormat.Api;
using TrashLib.Services.Radarr.CustomFormat.Models;
using TrashLib.Services.CustomFormat.Api;
using TrashLib.Services.CustomFormat.Models;
namespace TrashLib.Services.Radarr.CustomFormat.Processors.PersistenceSteps;
namespace TrashLib.Services.CustomFormat.Processors.PersistenceSteps;
internal class QualityProfileApiPersistenceStep : IQualityProfileApiPersistenceStep
{

@ -1,9 +0,0 @@
namespace TrashLib.Services.Radarr.CustomFormat;
public enum ApiOperationType
{
Create,
Update,
NoChange,
Delete
}

@ -1,82 +0,0 @@
using System.IO.Abstractions;
using System.Reactive.Linq;
using System.Reactive.Threading.Tasks;
using System.Text.RegularExpressions;
using Common.Extensions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Serilog;
using TrashLib.Repo;
using TrashLib.Services.Common.QualityDefinition;
using TrashLib.Services.Radarr.CustomFormat.Models;
using TrashLib.Services.Radarr.QualityDefinition;
namespace TrashLib.Services.Radarr.CustomFormat.Guide;
public class LocalRepoRadarrGuideService : IRadarrGuideService
{
private readonly IRepoPathsFactory _pathsFactory;
private readonly ILogger _log;
private readonly QualityGuideParser<RadarrQualityData> _parser;
public LocalRepoRadarrGuideService(IRepoPathsFactory pathsFactory, ILogger log)
{
_pathsFactory = pathsFactory;
_log = log;
_parser = new QualityGuideParser<RadarrQualityData>(log);
}
public ICollection<RadarrQualityData> GetQualities()
=> _parser.GetQualities(_pathsFactory.Create().RadarrQualityPaths);
public ICollection<CustomFormatData> GetCustomFormatData()
{
var paths = _pathsFactory.Create();
var jsonFiles = paths.RadarrCustomFormatPaths
.SelectMany(x => x.GetFiles("*.json"));
return jsonFiles.ToObservable()
.Select(x => Observable.Defer(() => LoadJsonFromFile(x)))
.Merge(8)
.NotNull()
.ToEnumerable()
.ToList();
}
private IObservable<CustomFormatData?> LoadJsonFromFile(IFileInfo file)
{
return Observable.Using(file.OpenText, x => x.ReadToEndAsync().ToObservable())
.Do(_ => _log.Debug("Parsing CF Json: {Name}", file.Name))
.Select(ParseCustomFormatData)
.Catch((JsonException e) =>
{
_log.Warning("Failed to parse JSON file: {File} ({Reason})", file.Name, e.Message);
return Observable.Empty<CustomFormatData>();
});
}
public static CustomFormatData ParseCustomFormatData(string guideData)
{
var obj = JObject.Parse(guideData);
var name = obj.ValueOrThrow<string>("name");
var trashId = obj.ValueOrThrow<string>("trash_id");
int? finalScore = null;
if (obj.TryGetValue("trash_score", out var score))
{
finalScore = (int) score;
}
// Remove any properties starting with "trash_". Those are metadata that are not meant for Radarr itself Radarr
// supposedly drops this anyway, but I prefer it to be removed. ToList() is important here since removing the
// property itself modifies the collection, and we don't want the collection to get modified while still looping
// over it.
foreach (var trashProperty in obj.Properties().Where(x => Regex.IsMatch(x.Name, @"^trash_")).ToList())
{
trashProperty.Remove();
}
return new CustomFormatData(name, trashId, finalScore, obj);
}
}

@ -1,8 +0,0 @@
using TrashLib.Services.Radarr.Config;
namespace TrashLib.Services.Radarr.CustomFormat;
public interface ICustomFormatUpdater
{
Task Process(bool isPreview, RadarrConfiguration config);
}

@ -1,4 +1,4 @@
namespace TrashLib.Services.Radarr.CustomFormat;
namespace TrashLib.Services.Radarr;
public interface IRadarrGuideDataLister
{

@ -1,7 +1,7 @@
using TrashLib.Services.Radarr.CustomFormat.Models;
using TrashLib.Services.CustomFormat.Models;
using TrashLib.Services.Radarr.QualityDefinition;
namespace TrashLib.Services.Radarr.CustomFormat.Guide;
namespace TrashLib.Services.Radarr;
public interface IRadarrGuideService
{

@ -0,0 +1,31 @@
using Serilog;
using TrashLib.Repo;
using TrashLib.Services.Common.QualityDefinition;
using TrashLib.Services.CustomFormat.Guide;
using TrashLib.Services.CustomFormat.Models;
using TrashLib.Services.Radarr.QualityDefinition;
namespace TrashLib.Services.Radarr;
public class LocalRepoRadarrGuideService : IRadarrGuideService
{
private readonly IRepoPathsFactory _pathsFactory;
private readonly ICustomFormatLoader _cfLoader;
private readonly QualityGuideParser<RadarrQualityData> _parser;
public LocalRepoRadarrGuideService(IRepoPathsFactory pathsFactory, ILogger log, ICustomFormatLoader cfLoader)
{
_pathsFactory = pathsFactory;
_cfLoader = cfLoader;
_parser = new QualityGuideParser<RadarrQualityData>(log);
}
public ICollection<RadarrQualityData> GetQualities()
=> _parser.GetQualities(_pathsFactory.Create().RadarrQualityPaths);
public ICollection<CustomFormatData> GetCustomFormatData()
{
var paths = _pathsFactory.Create();
return _cfLoader.LoadAllCustomFormatsAtPaths(paths.RadarrCustomFormatPaths);
}
}

@ -2,7 +2,6 @@ using CliFx.Infrastructure;
using Common.Extensions;
using Serilog;
using TrashLib.Services.Radarr.Config;
using TrashLib.Services.Radarr.CustomFormat.Guide;
using TrashLib.Services.Radarr.QualityDefinition.Api;
using TrashLib.Services.Radarr.QualityDefinition.Api.Objects;

@ -1,13 +1,5 @@
using Autofac;
using Autofac.Extras.AggregateService;
using TrashLib.Config.Services;
using TrashLib.Services.Radarr.Config;
using TrashLib.Services.Radarr.CustomFormat;
using TrashLib.Services.Radarr.CustomFormat.Api;
using TrashLib.Services.Radarr.CustomFormat.Guide;
using TrashLib.Services.Radarr.CustomFormat.Processors;
using TrashLib.Services.Radarr.CustomFormat.Processors.GuideSteps;
using TrashLib.Services.Radarr.CustomFormat.Processors.PersistenceSteps;
using TrashLib.Services.Radarr.QualityDefinition;
using TrashLib.Services.Radarr.QualityDefinition.Api;
@ -17,38 +9,11 @@ public class RadarrAutofacModule : Module
{
protected override void Load(ContainerBuilder builder)
{
// Services
builder.RegisterType<QualityDefinitionService>().As<IQualityDefinitionService>();
builder.RegisterType<CustomFormatService>().As<ICustomFormatService>();
builder.RegisterType<QualityProfileService>().As<IQualityProfileService>();
// Configuration
builder.RegisterType<RadarrGuideDataLister>().As<IRadarrGuideDataLister>();
builder.RegisterType<RadarrValidationMessages>().As<IRadarrValidationMessages>();
builder.RegisterType<ServerInfo>().As<IServerInfo>();
// Quality Definition Support
builder.RegisterType<RadarrQualityDefinitionUpdater>().As<IRadarrQualityDefinitionUpdater>();
// Custom Format Support
builder.RegisterType<CustomFormatUpdater>().As<ICustomFormatUpdater>();
builder.RegisterType<LocalRepoRadarrGuideService>().As<IRadarrGuideService>();
builder.RegisterType<CachePersister>().As<ICachePersister>();
builder.RegisterType<RadarrGuideDataLister>().As<IRadarrGuideDataLister>();
// Guide Processor
// todo: register as singleton to avoid parsing guide multiple times when using 2 or more instances in config
builder.RegisterType<GuideProcessor>().As<IGuideProcessor>();
builder.RegisterAggregateService<IGuideProcessorSteps>();
builder.RegisterType<CustomFormatStep>().As<ICustomFormatStep>();
builder.RegisterType<ConfigStep>().As<IConfigStep>();
builder.RegisterType<QualityProfileStep>().As<IQualityProfileStep>();
// Persistence Processor
builder.RegisterType<PersistenceProcessor>().As<IPersistenceProcessor>();
builder.RegisterAggregateService<IPersistenceProcessorSteps>();
builder.RegisterType<JsonTransactionStep>().As<IJsonTransactionStep>();
builder.RegisterType<CustomFormatApiPersistenceStep>().As<ICustomFormatApiPersistenceStep>();
builder.RegisterType<QualityProfileApiPersistenceStep>().As<IQualityProfileApiPersistenceStep>();
}
}

@ -1,36 +1,28 @@
using CliFx.Infrastructure;
using JetBrains.Annotations;
using MoreLinq;
using TrashLib.Services.Radarr.CustomFormat.Guide;
using TrashLib.Services.Common;
namespace TrashLib.Services.Radarr.CustomFormat;
namespace TrashLib.Services.Radarr;
[UsedImplicitly]
public class RadarrGuideDataLister : IRadarrGuideDataLister
{
private readonly IConsole _console;
private readonly IRadarrGuideService _guide;
private readonly IGuideDataLister _guideLister;
public RadarrGuideDataLister(IConsole console, IRadarrGuideService guide)
public RadarrGuideDataLister(
IConsole console,
IRadarrGuideService guide,
IGuideDataLister guideLister)
{
_console = console;
_guide = guide;
_guideLister = guideLister;
}
public void ListCustomFormats()
{
_console.Output.WriteLine("\nList of Custom Formats in the TRaSH Guides:\n");
var profilesFromGuide = _guide.GetCustomFormatData();
foreach (var profile in profilesFromGuide)
{
_console.Output.WriteLine($" - {profile.TrashId} # {profile.Name}");
}
_console.Output.WriteLine(
"\nThe above Custom Formats are in YAML format and ready to be copied & pasted " +
"under the `trash_ids:` property.");
}
public void ListCustomFormats() => _guideLister.ListCustomFormats(_guide.GetCustomFormatData());
public void ListQualities()
{

@ -1,10 +1,12 @@
using TrashLib.Config.Services;
using TrashLib.Services.Radarr.Config;
namespace TrashLib.Services.Sonarr.Config;
public class SonarrConfiguration : ServiceConfiguration
{
public IList<ReleaseProfileConfig> ReleaseProfiles { get; init; } = Array.Empty<ReleaseProfileConfig>();
public ICollection<CustomFormatConfig> CustomFormats { get; init; } = new List<CustomFormatConfig>();
public string QualityDefinition { get; init; } = "";
}

@ -5,4 +5,5 @@ public interface ISonarrGuideDataLister
void ListReleaseProfiles();
void ListTerms(string releaseProfileId);
void ListQualities();
void ListCustomFormats();
}

@ -1,3 +1,4 @@
using TrashLib.Services.CustomFormat.Models;
using TrashLib.Services.Sonarr.QualityDefinition;
namespace TrashLib.Services.Sonarr.ReleaseProfile.Guide;
@ -7,4 +8,5 @@ public interface ISonarrGuideService
IReadOnlyCollection<ReleaseProfileData> GetReleaseProfileData();
ReleaseProfileData? GetUnfilteredProfileById(string trashId);
ICollection<SonarrQualityData> GetQualities();
IEnumerable<CustomFormatData> GetCustomFormatData();
}

@ -5,6 +5,8 @@ using Newtonsoft.Json;
using Serilog;
using TrashLib.Repo;
using TrashLib.Services.Common.QualityDefinition;
using TrashLib.Services.CustomFormat.Guide;
using TrashLib.Services.CustomFormat.Models;
using TrashLib.Services.Sonarr.QualityDefinition;
using TrashLib.Services.Sonarr.ReleaseProfile.Filters;
@ -12,26 +14,37 @@ namespace TrashLib.Services.Sonarr.ReleaseProfile.Guide;
public class LocalRepoSonarrGuideService : ISonarrGuideService
{
private readonly IRepoPathsFactory _pathFactory;
private readonly IRepoPathsFactory _pathsFactory;
private readonly ILogger _log;
private readonly ICustomFormatLoader _cfLoader;
private readonly Lazy<IEnumerable<ReleaseProfileData>> _data;
private readonly QualityGuideParser<SonarrQualityData> _parser;
public LocalRepoSonarrGuideService(IRepoPathsFactory pathFactory, ILogger log)
public LocalRepoSonarrGuideService(
IRepoPathsFactory pathsFactory,
ILogger log,
ICustomFormatLoader cfLoader)
{
_pathFactory = pathFactory;
_pathsFactory = pathsFactory;
_log = log;
_cfLoader = cfLoader;
_data = new Lazy<IEnumerable<ReleaseProfileData>>(GetReleaseProfileDataImpl);
_parser = new QualityGuideParser<SonarrQualityData>(log);
}
public ICollection<SonarrQualityData> GetQualities()
=> _parser.GetQualities(_pathFactory.Create().SonarrQualityPaths);
=> _parser.GetQualities(_pathsFactory.Create().SonarrQualityPaths);
public IEnumerable<CustomFormatData> GetCustomFormatData()
{
var paths = _pathsFactory.Create();
return _cfLoader.LoadAllCustomFormatsAtPaths(paths.SonarrCustomFormatPaths);
}
private IEnumerable<ReleaseProfileData> GetReleaseProfileDataImpl()
{
var converter = new TermDataConverter();
var paths = _pathFactory.Create();
var paths = _pathsFactory.Create();
var tasks = paths.SonarrReleaseProfilePaths
.SelectMany(x => x.GetFiles("*.json"))
.Select(x => LoadAndParseFile(x, converter));

@ -3,6 +3,7 @@ using CliFx.Infrastructure;
using JetBrains.Annotations;
using MoreLinq;
using Serilog;
using TrashLib.Services.Common;
using TrashLib.Services.Sonarr.ReleaseProfile;
using TrashLib.Services.Sonarr.ReleaseProfile.Guide;
@ -14,14 +15,22 @@ public class SonarrGuideDataLister : ISonarrGuideDataLister
private readonly IConsole _console;
private readonly ISonarrGuideService _guide;
private readonly ILogger _log;
private readonly IGuideDataLister _guideLister;
public SonarrGuideDataLister(IConsole console, ISonarrGuideService guide, ILogger log)
public SonarrGuideDataLister(
IConsole console,
ISonarrGuideService guide,
ILogger log,
IGuideDataLister guideLister)
{
_console = console;
_guide = guide;
_log = log;
_guideLister = guideLister;
}
public void ListCustomFormats() => _guideLister.ListCustomFormats(_guide.GetCustomFormatData());
public void ListReleaseProfiles()
{
_console.Output.WriteLine("\nList of Release Profiles in the TRaSH Guides:\n");

Loading…
Cancel
Save