commit
cd6636d44c
@ -1,7 +1,7 @@
|
|||||||
<component name="ProjectRunConfigurationManager">
|
<component name="ProjectRunConfigurationManager">
|
||||||
<configuration default="false" name="create-config (custom)" type="DotNetProject" factoryName=".NET Project">
|
<configuration default="false" name="config create -p custom" type="DotNetProject" factoryName=".NET Project">
|
||||||
<option name="EXE_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0/recyclarr.exe" />
|
<option name="EXE_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0/recyclarr.exe" />
|
||||||
<option name="PROGRAM_PARAMETERS" value="create-config --path ./custom-config.yml" />
|
<option name="PROGRAM_PARAMETERS" value="config create -p ./custom-config.yml" />
|
||||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0" />
|
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0" />
|
||||||
<option name="PASS_PARENT_ENVS" value="1" />
|
<option name="PASS_PARENT_ENVS" value="1" />
|
||||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
@ -1,8 +1,8 @@
|
|||||||
<component name="ProjectRunConfigurationManager">
|
<component name="ProjectRunConfigurationManager">
|
||||||
<configuration default="false" name="create-config (default)" type="DotNetProject" factoryName=".NET Project">
|
<configuration default="false" name="create-config" type="DotNetProject" factoryName=".NET Project">
|
||||||
<option name="EXE_PATH" value="$PROJECT_DIR$/Recyclarr/bin/Debug/net7.0/recyclarr.exe" />
|
<option name="EXE_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0/recyclarr.exe" />
|
||||||
<option name="PROGRAM_PARAMETERS" value="create-config" />
|
<option name="PROGRAM_PARAMETERS" value="create-config" />
|
||||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/Recyclarr/bin/Debug/net7.0" />
|
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0" />
|
||||||
<option name="PASS_PARENT_ENVS" value="1" />
|
<option name="PASS_PARENT_ENVS" value="1" />
|
||||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||||
<option name="USE_MONO" value="0" />
|
<option name="USE_MONO" value="0" />
|
@ -0,0 +1,20 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="list custom-formats radarr" type="DotNetProject" factoryName=".NET Project">
|
||||||
|
<option name="EXE_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0/recyclarr.exe" />
|
||||||
|
<option name="PROGRAM_PARAMETERS" value="list custom-formats radarr" />
|
||||||
|
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0" />
|
||||||
|
<option name="PASS_PARENT_ENVS" value="1" />
|
||||||
|
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||||
|
<option name="USE_MONO" value="0" />
|
||||||
|
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||||
|
<option name="PROJECT_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/Recyclarr.Cli.csproj" />
|
||||||
|
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||||
|
<option name="PROJECT_TFM" value="net7.0" />
|
||||||
|
<method v="2">
|
||||||
|
<option name="Build" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
|
</component>
|
@ -0,0 +1,20 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="list custom-formats sonarr" type="DotNetProject" factoryName=".NET Project">
|
||||||
|
<option name="EXE_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0/recyclarr.exe" />
|
||||||
|
<option name="PROGRAM_PARAMETERS" value="list custom-formats sonarr" />
|
||||||
|
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0" />
|
||||||
|
<option name="PASS_PARENT_ENVS" value="1" />
|
||||||
|
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||||
|
<option name="USE_MONO" value="0" />
|
||||||
|
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||||
|
<option name="PROJECT_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/Recyclarr.Cli.csproj" />
|
||||||
|
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||||
|
<option name="PROJECT_TFM" value="net7.0" />
|
||||||
|
<method v="2">
|
||||||
|
<option name="Build" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
|
</component>
|
@ -0,0 +1,20 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="list qualities radarr" type="DotNetProject" factoryName=".NET Project">
|
||||||
|
<option name="EXE_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0/recyclarr.exe" />
|
||||||
|
<option name="PROGRAM_PARAMETERS" value="list qualities radarr" />
|
||||||
|
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0" />
|
||||||
|
<option name="PASS_PARENT_ENVS" value="1" />
|
||||||
|
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||||
|
<option name="USE_MONO" value="0" />
|
||||||
|
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||||
|
<option name="PROJECT_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/Recyclarr.Cli.csproj" />
|
||||||
|
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||||
|
<option name="PROJECT_TFM" value="net7.0" />
|
||||||
|
<method v="2">
|
||||||
|
<option name="Build" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
|
</component>
|
@ -0,0 +1,20 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="list qualities sonarr" type="DotNetProject" factoryName=".NET Project">
|
||||||
|
<option name="EXE_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0/recyclarr.exe" />
|
||||||
|
<option name="PROGRAM_PARAMETERS" value="list qualities sonarr" />
|
||||||
|
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0" />
|
||||||
|
<option name="PASS_PARENT_ENVS" value="1" />
|
||||||
|
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||||
|
<option name="USE_MONO" value="0" />
|
||||||
|
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||||
|
<option name="PROJECT_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/Recyclarr.Cli.csproj" />
|
||||||
|
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||||
|
<option name="PROJECT_TFM" value="net7.0" />
|
||||||
|
<method v="2">
|
||||||
|
<option name="Build" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
|
</component>
|
@ -0,0 +1,20 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="list release-profiles --terms" type="DotNetProject" factoryName=".NET Project">
|
||||||
|
<option name="EXE_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0/recyclarr.exe" />
|
||||||
|
<option name="PROGRAM_PARAMETERS" value="list release-profiles --terms EBC725268D687D588A20CBC5F97E538B" />
|
||||||
|
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0" />
|
||||||
|
<option name="PASS_PARENT_ENVS" value="1" />
|
||||||
|
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||||
|
<option name="USE_MONO" value="0" />
|
||||||
|
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||||
|
<option name="PROJECT_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/Recyclarr.Cli.csproj" />
|
||||||
|
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||||
|
<option name="PROJECT_TFM" value="net7.0" />
|
||||||
|
<method v="2">
|
||||||
|
<option name="Build" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
|
</component>
|
@ -0,0 +1,20 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="list release-profiles" type="DotNetProject" factoryName=".NET Project">
|
||||||
|
<option name="EXE_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0/recyclarr.exe" />
|
||||||
|
<option name="PROGRAM_PARAMETERS" value="list release-profiles" />
|
||||||
|
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0" />
|
||||||
|
<option name="PASS_PARENT_ENVS" value="1" />
|
||||||
|
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||||
|
<option name="USE_MONO" value="0" />
|
||||||
|
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||||
|
<option name="PROJECT_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/Recyclarr.Cli.csproj" />
|
||||||
|
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||||
|
<option name="PROJECT_TFM" value="net7.0" />
|
||||||
|
<method v="2">
|
||||||
|
<option name="Build" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
|
</component>
|
@ -0,0 +1,20 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="migrate" type="DotNetProject" factoryName=".NET Project">
|
||||||
|
<option name="EXE_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0/recyclarr.exe" />
|
||||||
|
<option name="PROGRAM_PARAMETERS" value="migrate" />
|
||||||
|
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0" />
|
||||||
|
<option name="PASS_PARENT_ENVS" value="1" />
|
||||||
|
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||||
|
<option name="USE_MONO" value="0" />
|
||||||
|
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||||
|
<option name="PROJECT_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/Recyclarr.Cli.csproj" />
|
||||||
|
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||||
|
<option name="PROJECT_TFM" value="net7.0" />
|
||||||
|
<method v="2">
|
||||||
|
<option name="Build" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
|
</component>
|
@ -0,0 +1,20 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="recyclarr" type="DotNetProject" factoryName=".NET Project">
|
||||||
|
<option name="EXE_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0/recyclarr.exe" />
|
||||||
|
<option name="PROGRAM_PARAMETERS" value="" />
|
||||||
|
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0" />
|
||||||
|
<option name="PASS_PARENT_ENVS" value="1" />
|
||||||
|
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||||
|
<option name="USE_MONO" value="0" />
|
||||||
|
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||||
|
<option name="PROJECT_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/Recyclarr.Cli.csproj" />
|
||||||
|
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||||
|
<option name="PROJECT_TFM" value="net7.0" />
|
||||||
|
<method v="2">
|
||||||
|
<option name="Build" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
|
</component>
|
@ -0,0 +1,20 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="sync -c custom" type="DotNetProject" factoryName=".NET Project">
|
||||||
|
<option name="EXE_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0/recyclarr.exe" />
|
||||||
|
<option name="PROGRAM_PARAMETERS" value="sync -c custom-config.yml" />
|
||||||
|
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0" />
|
||||||
|
<option name="PASS_PARENT_ENVS" value="1" />
|
||||||
|
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||||
|
<option name="USE_MONO" value="0" />
|
||||||
|
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||||
|
<option name="PROJECT_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/Recyclarr.Cli.csproj" />
|
||||||
|
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||||
|
<option name="PROJECT_TFM" value="net7.0" />
|
||||||
|
<method v="2">
|
||||||
|
<option name="Build" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
|
</component>
|
@ -0,0 +1,20 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="sync" type="DotNetProject" factoryName=".NET Project">
|
||||||
|
<option name="EXE_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0/recyclarr.exe" />
|
||||||
|
<option name="PROGRAM_PARAMETERS" value="sync" />
|
||||||
|
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0" />
|
||||||
|
<option name="PASS_PARENT_ENVS" value="1" />
|
||||||
|
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||||
|
<option name="USE_MONO" value="0" />
|
||||||
|
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||||
|
<option name="PROJECT_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/Recyclarr.Cli.csproj" />
|
||||||
|
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||||
|
<option name="PROJECT_TFM" value="net7.0" />
|
||||||
|
<method v="2">
|
||||||
|
<option name="Build" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
|
</component>
|
@ -0,0 +1,20 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="sync -i -i" type="DotNetProject" factoryName=".NET Project">
|
||||||
|
<option name="EXE_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0/recyclarr.exe" />
|
||||||
|
<option name="PROGRAM_PARAMETERS" value="sync -i movies -i v4" />
|
||||||
|
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0" />
|
||||||
|
<option name="PASS_PARENT_ENVS" value="1" />
|
||||||
|
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||||
|
<option name="USE_MONO" value="0" />
|
||||||
|
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||||
|
<option name="PROJECT_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/Recyclarr.Cli.csproj" />
|
||||||
|
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||||
|
<option name="PROJECT_TFM" value="net7.0" />
|
||||||
|
<method v="2">
|
||||||
|
<option name="Build" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
|
</component>
|
@ -0,0 +1,17 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using Recyclarr.Cli.TestLibrary;
|
||||||
|
|
||||||
|
namespace Recyclarr.Cli.Tests;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
[Parallelizable(ParallelScope.All)]
|
||||||
|
public class AutoMapperConfigurationTest : IntegrationFixture
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void Automapper_config_is_valid()
|
||||||
|
{
|
||||||
|
var mapper = Resolve<MapperConfiguration>();
|
||||||
|
mapper.AssertConfigurationIsValid();
|
||||||
|
}
|
||||||
|
}
|
@ -1,44 +0,0 @@
|
|||||||
using System.IO.Abstractions;
|
|
||||||
using System.IO.Abstractions.Extensions;
|
|
||||||
using FluentAssertions;
|
|
||||||
using NUnit.Framework;
|
|
||||||
using Recyclarr.Cli.Command;
|
|
||||||
using Recyclarr.Cli.TestLibrary;
|
|
||||||
|
|
||||||
// ReSharper disable MethodHasAsyncOverload
|
|
||||||
|
|
||||||
namespace Recyclarr.Cli.Tests.Command;
|
|
||||||
|
|
||||||
[TestFixture]
|
|
||||||
[Parallelizable(ParallelScope.All)]
|
|
||||||
public class CreateConfigCommandTest : IntegrationFixture
|
|
||||||
{
|
|
||||||
[Test]
|
|
||||||
public async Task Config_file_created_when_using_default_path()
|
|
||||||
{
|
|
||||||
var sut = new CreateConfigCommand();
|
|
||||||
|
|
||||||
await sut.Process(Container);
|
|
||||||
|
|
||||||
var file = Fs.GetFile(Paths.ConfigPath.FullName);
|
|
||||||
file.Should().NotBeNull();
|
|
||||||
file.Contents.Should().NotBeEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public async Task Config_file_created_when_using_user_specified_path()
|
|
||||||
{
|
|
||||||
var sut = new CreateConfigCommand();
|
|
||||||
var ymlPath = Fs.CurrentDirectory()
|
|
||||||
.SubDirectory("user")
|
|
||||||
.SubDirectory("specified")
|
|
||||||
.File("file.yml").FullName;
|
|
||||||
|
|
||||||
sut.AppDataDirectory = ymlPath;
|
|
||||||
await sut.Process(Container);
|
|
||||||
|
|
||||||
var file = Fs.GetFile(ymlPath);
|
|
||||||
file.Should().NotBeNull();
|
|
||||||
file.Contents.Should().NotBeEmpty();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,79 +0,0 @@
|
|||||||
using System.IO.Abstractions;
|
|
||||||
using CliFx.Exceptions;
|
|
||||||
using CliFx.Infrastructure;
|
|
||||||
using FluentAssertions;
|
|
||||||
using NUnit.Framework;
|
|
||||||
using Recyclarr.Cli.Command;
|
|
||||||
using Recyclarr.Cli.TestLibrary;
|
|
||||||
using Recyclarr.Common.TestLibrary;
|
|
||||||
using Recyclarr.TestLibrary.AutoFixture;
|
|
||||||
using Recyclarr.TrashLib.Repo;
|
|
||||||
|
|
||||||
namespace Recyclarr.Cli.Tests.Command;
|
|
||||||
|
|
||||||
[TestFixture]
|
|
||||||
[Parallelizable(ParallelScope.All)]
|
|
||||||
public class SonarrCommandTest : IntegrationFixture
|
|
||||||
{
|
|
||||||
[Test, AutoMockData]
|
|
||||||
public async Task List_terms_without_value_fails(
|
|
||||||
IConsole console,
|
|
||||||
SonarrCommand sut)
|
|
||||||
{
|
|
||||||
sut.ListReleaseProfiles = false;
|
|
||||||
|
|
||||||
// When `--list-terms` is specified on the command line without a value, it gets a `null` value assigned.
|
|
||||||
sut.ListTerms = null;
|
|
||||||
|
|
||||||
var act = async () => await sut.Process(Container);
|
|
||||||
|
|
||||||
await act.Should().ThrowAsync<CommandException>();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test, AutoMockData]
|
|
||||||
public async Task List_terms_with_empty_value_fails(
|
|
||||||
IConsole console,
|
|
||||||
SonarrCommand sut)
|
|
||||||
{
|
|
||||||
sut.ListReleaseProfiles = false;
|
|
||||||
|
|
||||||
// If the user specifies a blank string as the value, it should still fail.
|
|
||||||
sut.ListTerms = "";
|
|
||||||
|
|
||||||
var act = async () => await sut.Process(Container);
|
|
||||||
|
|
||||||
await act.Should().ThrowAsync<CommandException>();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public async Task List_terms_uses_specified_trash_id()
|
|
||||||
{
|
|
||||||
var repoPaths = Resolve<IRepoPathsFactory>().Create();
|
|
||||||
var cfDir = repoPaths.SonarrReleaseProfilePaths.First();
|
|
||||||
Fs.AddFileFromResource(cfDir.File("optionals.json"), "optionals.json");
|
|
||||||
|
|
||||||
var sut = new SonarrCommand
|
|
||||||
{
|
|
||||||
ListReleaseProfiles = false,
|
|
||||||
ListTerms = "76e060895c5b8a765c310933da0a5357"
|
|
||||||
};
|
|
||||||
|
|
||||||
await sut.Process(Container);
|
|
||||||
|
|
||||||
Console.ReadOutputString().Should().Contain("List of Terms");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public async Task List_release_profiles_is_invoked()
|
|
||||||
{
|
|
||||||
var sut = new SonarrCommand
|
|
||||||
{
|
|
||||||
ListReleaseProfiles = true,
|
|
||||||
ListTerms = null
|
|
||||||
};
|
|
||||||
|
|
||||||
await sut.Process(Container);
|
|
||||||
|
|
||||||
Console.ReadOutputString().Should().Contain("List of Release Profiles");
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,92 +0,0 @@
|
|||||||
using Autofac;
|
|
||||||
using CliFx;
|
|
||||||
using CliFx.Attributes;
|
|
||||||
using CliFx.Exceptions;
|
|
||||||
using CliFx.Infrastructure;
|
|
||||||
using Flurl.Http;
|
|
||||||
using JetBrains.Annotations;
|
|
||||||
using MoreLinq.Extensions;
|
|
||||||
using Recyclarr.Cli.Command.Setup;
|
|
||||||
using Recyclarr.Cli.Logging;
|
|
||||||
using Recyclarr.TrashLib.Http;
|
|
||||||
using Recyclarr.TrashLib.Repo.VersionControl;
|
|
||||||
using Recyclarr.TrashLib.Startup;
|
|
||||||
using Serilog;
|
|
||||||
using Serilog.Events;
|
|
||||||
|
|
||||||
namespace Recyclarr.Cli.Command;
|
|
||||||
|
|
||||||
public abstract class BaseCommand : ICommand
|
|
||||||
{
|
|
||||||
// Not explicitly defined as a Command here because for legacy reasons, different subcommands expose this in
|
|
||||||
// different ways to the user.
|
|
||||||
public abstract string? AppDataDirectory { get; set; }
|
|
||||||
|
|
||||||
[CommandOption("debug", 'd', Description =
|
|
||||||
"Display additional logs useful for development/debug purposes.")]
|
|
||||||
// ReSharper disable once MemberCanBeProtected.Global
|
|
||||||
public bool Debug { get; [UsedImplicitly] set; } = false;
|
|
||||||
|
|
||||||
protected ILogger Logger { get; private set; } = Log.Logger;
|
|
||||||
|
|
||||||
protected virtual void RegisterServices(ContainerBuilder builder)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual async ValueTask ExecuteAsync(IConsole console)
|
|
||||||
{
|
|
||||||
// Must happen first because everything can use the logger.
|
|
||||||
var logLevel = Debug ? LogEventLevel.Debug : LogEventLevel.Information;
|
|
||||||
|
|
||||||
await using var container = CompositionRoot.Setup(builder =>
|
|
||||||
{
|
|
||||||
builder.RegisterInstance(console).As<IConsole>().ExternallyOwned();
|
|
||||||
|
|
||||||
builder.Register(c => c.Resolve<DefaultAppDataSetup>().CreateAppPaths(AppDataDirectory))
|
|
||||||
.As<IAppPaths>()
|
|
||||||
.SingleInstance();
|
|
||||||
|
|
||||||
builder.Register(c => c.Resolve<LoggerFactory>().Create(logLevel))
|
|
||||||
.As<ILogger>()
|
|
||||||
.SingleInstance();
|
|
||||||
|
|
||||||
RegisterServices(builder);
|
|
||||||
});
|
|
||||||
|
|
||||||
Logger = container.Resolve<ILogger>();
|
|
||||||
var tasks = container.Resolve<IOrderedEnumerable<IBaseCommandSetupTask>>().ToArray();
|
|
||||||
tasks.ForEach(x => x.OnStart());
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await Process(container);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
switch (e)
|
|
||||||
{
|
|
||||||
case GitCmdException e2:
|
|
||||||
Logger.Error(e2, "Non-zero exit code {ExitCode} while executing Git command: {Error}",
|
|
||||||
e2.ExitCode, e2.Error);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case FlurlHttpException e2:
|
|
||||||
Logger.Error("HTTP error: {Message}", e2.SanitizedExceptionMessage());
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
Logger.Error("{Message}", e.Message);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.Debug("Exception Stacktrace: {Stacktrace}", e.StackTrace);
|
|
||||||
throw new CommandException("Exiting due to exception");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
tasks.Reverse().ForEach(x => x.OnFinish());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract Task Process(ILifetimeScope container);
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
using System.IO.Abstractions;
|
|
||||||
using Autofac;
|
|
||||||
using CliFx.Attributes;
|
|
||||||
using CliFx.Exceptions;
|
|
||||||
using JetBrains.Annotations;
|
|
||||||
using Recyclarr.Common;
|
|
||||||
using Recyclarr.Common.Extensions;
|
|
||||||
using Recyclarr.TrashLib.Startup;
|
|
||||||
using Serilog;
|
|
||||||
|
|
||||||
namespace Recyclarr.Cli.Command;
|
|
||||||
|
|
||||||
[Command("create-config", Description = "Create a starter YAML configuration file")]
|
|
||||||
[UsedImplicitly]
|
|
||||||
public class CreateConfigCommand : BaseCommand
|
|
||||||
{
|
|
||||||
[CommandOption("path", 'p', Description =
|
|
||||||
"Path where the new YAML file should be created. Must include the filename (e.g. path/to/config.yml). " +
|
|
||||||
"File must not already exist. If not specified, uses the default path of `recyclarr.yml` in the app data " +
|
|
||||||
"directory")]
|
|
||||||
public override string? AppDataDirectory { get; set; }
|
|
||||||
|
|
||||||
public override async Task Process(ILifetimeScope container)
|
|
||||||
{
|
|
||||||
var fs = container.Resolve<IFileSystem>();
|
|
||||||
var paths = container.Resolve<IAppPaths>();
|
|
||||||
var log = container.Resolve<ILogger>();
|
|
||||||
|
|
||||||
var reader = new ResourceDataReader(typeof(Program));
|
|
||||||
var ymlData = reader.ReadData("config-template.yml");
|
|
||||||
var configFile = AppDataDirectory is not null
|
|
||||||
? fs.FileInfo.New(AppDataDirectory)
|
|
||||||
: paths.ConfigPath;
|
|
||||||
|
|
||||||
if (configFile.Exists)
|
|
||||||
{
|
|
||||||
throw new CommandException(
|
|
||||||
$"The file {configFile} already exists. Please choose another path or delete/move the existing " +
|
|
||||||
"file and run this command again.");
|
|
||||||
}
|
|
||||||
|
|
||||||
configFile.CreateParentDirectory();
|
|
||||||
await using var stream = configFile.CreateText();
|
|
||||||
await stream.WriteAsync(ymlData);
|
|
||||||
log.Information("Created configuration at: {Path}", configFile);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
namespace Recyclarr.Cli.Command;
|
|
||||||
|
|
||||||
public interface IServiceCommand
|
|
||||||
{
|
|
||||||
string Name { get; }
|
|
||||||
}
|
|
@ -1,49 +0,0 @@
|
|||||||
using System.Text;
|
|
||||||
using Autofac;
|
|
||||||
using CliFx.Attributes;
|
|
||||||
using CliFx.Exceptions;
|
|
||||||
using JetBrains.Annotations;
|
|
||||||
using Recyclarr.Cli.Migration;
|
|
||||||
|
|
||||||
namespace Recyclarr.Cli.Command;
|
|
||||||
|
|
||||||
[Command("migrate", Description = "Perform any migration steps that may be needed between versions")]
|
|
||||||
[UsedImplicitly]
|
|
||||||
public class MigrateCommand : BaseCommand
|
|
||||||
{
|
|
||||||
[CommandOption("app-data", Description =
|
|
||||||
"Explicitly specify the location of the recyclarr application data directory. " +
|
|
||||||
"Mainly for usage in Docker; not recommended for normal use.")]
|
|
||||||
public override string? AppDataDirectory { get; set; }
|
|
||||||
|
|
||||||
public override Task Process(ILifetimeScope container)
|
|
||||||
{
|
|
||||||
var migration = container.Resolve<IMigrationExecutor>();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
migration.PerformAllMigrationSteps(Debug);
|
|
||||||
}
|
|
||||||
catch (MigrationException e)
|
|
||||||
{
|
|
||||||
var msg = new StringBuilder();
|
|
||||||
msg.AppendLine("Fatal exception during migration step. Details are below.\n");
|
|
||||||
msg.AppendLine($"Step That Failed: {e.OperationDescription}");
|
|
||||||
msg.AppendLine($"Failure Reason: {e.OriginalException.Message}");
|
|
||||||
|
|
||||||
// ReSharper disable once InvertIf
|
|
||||||
if (e.Remediation.Any())
|
|
||||||
{
|
|
||||||
msg.AppendLine("\nPossible remediation steps:");
|
|
||||||
foreach (var remedy in e.Remediation)
|
|
||||||
{
|
|
||||||
msg.AppendLine($" - {remedy}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new CommandException(msg.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,99 +0,0 @@
|
|||||||
using Autofac;
|
|
||||||
using CliFx.Attributes;
|
|
||||||
using CliFx.Infrastructure;
|
|
||||||
using JetBrains.Annotations;
|
|
||||||
using Recyclarr.Cli.Config;
|
|
||||||
using Recyclarr.TrashLib.Config.Services;
|
|
||||||
using Recyclarr.TrashLib.Http;
|
|
||||||
using Recyclarr.TrashLib.Services.CustomFormat;
|
|
||||||
using Recyclarr.TrashLib.Services.QualitySize;
|
|
||||||
using Recyclarr.TrashLib.Services.Radarr;
|
|
||||||
using Recyclarr.TrashLib.Services.Radarr.Config;
|
|
||||||
using Serilog;
|
|
||||||
|
|
||||||
namespace Recyclarr.Cli.Command;
|
|
||||||
|
|
||||||
[Command("radarr", Description = "Perform operations on a Radarr instance")]
|
|
||||||
[UsedImplicitly]
|
|
||||||
internal class RadarrCommand : ServiceCommand
|
|
||||||
{
|
|
||||||
// ReSharper disable MemberCanBePrivate.Global
|
|
||||||
|
|
||||||
[CommandOption("list-custom-formats", Description =
|
|
||||||
"List available custom formats from the guide in YAML format.")]
|
|
||||||
public bool ListCustomFormats { get; [UsedImplicitly] set; }
|
|
||||||
|
|
||||||
[CommandOption("list-qualities", Description =
|
|
||||||
"List available quality definition types from the guide.")]
|
|
||||||
public bool ListQualities { get; [UsedImplicitly] set; }
|
|
||||||
|
|
||||||
// ReSharper restore MemberCanBePrivate.Global
|
|
||||||
|
|
||||||
public override string Name => "Radarr";
|
|
||||||
|
|
||||||
public override async Task Process(ILifetimeScope container)
|
|
||||||
{
|
|
||||||
await base.Process(container);
|
|
||||||
|
|
||||||
var lister = container.Resolve<IRadarrGuideDataLister>();
|
|
||||||
var log = container.Resolve<ILogger>();
|
|
||||||
var guideService = container.Resolve<IRadarrGuideService>();
|
|
||||||
var console = container.Resolve<IConsole>();
|
|
||||||
|
|
||||||
if (ListCustomFormats)
|
|
||||||
{
|
|
||||||
lister.ListCustomFormats();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ListQualities)
|
|
||||||
{
|
|
||||||
lister.ListQualities();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var configFinder = container.Resolve<IConfigurationFinder>();
|
|
||||||
var configLoader = container.Resolve<IConfigurationLoader<RadarrConfiguration>>();
|
|
||||||
foreach (var config in configLoader.LoadMany(configFinder.GetConfigFiles(Config), "radarr"))
|
|
||||||
{
|
|
||||||
await using var scope = container.BeginLifetimeScope(builder =>
|
|
||||||
{
|
|
||||||
builder.RegisterInstance(config).As<IServiceConfiguration>();
|
|
||||||
});
|
|
||||||
|
|
||||||
var serverName = Name;
|
|
||||||
var instanceName = config.Name ?? FlurlLogging.SanitizeUrl(config.BaseUrl);
|
|
||||||
|
|
||||||
await console.Output.WriteLineAsync($@"
|
|
||||||
===========================================
|
|
||||||
Processing {serverName} Server: [{instanceName}]
|
|
||||||
===========================================
|
|
||||||
");
|
|
||||||
|
|
||||||
log.Debug("Processing {Server} server {Name}", serverName, instanceName);
|
|
||||||
|
|
||||||
var validator = scope.Resolve<ConfigValidationExecutor>();
|
|
||||||
if (!validator.Validate(config))
|
|
||||||
{
|
|
||||||
log.Error("Due to validation failure, this instance will be skipped");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReSharper disable InvertIf
|
|
||||||
|
|
||||||
if (config.QualityDefinition != null)
|
|
||||||
{
|
|
||||||
var updater = scope.Resolve<IQualitySizeUpdater>();
|
|
||||||
await updater.Process(Preview, config.QualityDefinition, guideService);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.CustomFormats.Count > 0)
|
|
||||||
{
|
|
||||||
var updater = scope.Resolve<ICustomFormatUpdater>();
|
|
||||||
await updater.Process(Preview, config.CustomFormats, guideService);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReSharper restore InvertIf
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,46 +0,0 @@
|
|||||||
using Autofac;
|
|
||||||
using CliFx.Attributes;
|
|
||||||
using JetBrains.Annotations;
|
|
||||||
using Recyclarr.Cli.Migration;
|
|
||||||
using Recyclarr.TrashLib.Repo;
|
|
||||||
|
|
||||||
namespace Recyclarr.Cli.Command;
|
|
||||||
|
|
||||||
public abstract class ServiceCommand : BaseCommand, IServiceCommand
|
|
||||||
{
|
|
||||||
[CommandOption("preview", 'p', Description =
|
|
||||||
"Only display the processed markdown results without making any API calls.")]
|
|
||||||
// ReSharper disable once MemberCanBeProtected.Global
|
|
||||||
public bool Preview { get; [UsedImplicitly] set; } = false;
|
|
||||||
|
|
||||||
[CommandOption("config", 'c', Description =
|
|
||||||
"One or more YAML config files to use. All configs will be used and settings are additive. " +
|
|
||||||
"If not specified, the script will look for `recyclarr.yml` in the same directory as the executable.")]
|
|
||||||
// ReSharper disable once MemberCanBeProtected.Global
|
|
||||||
public IReadOnlyCollection<string> Config { get; [UsedImplicitly] set; } = new List<string>();
|
|
||||||
|
|
||||||
[CommandOption("app-data", Description =
|
|
||||||
"Explicitly specify the location of the recyclarr application data directory. " +
|
|
||||||
"Mainly for usage in Docker; not recommended for normal use.")]
|
|
||||||
public override string? AppDataDirectory { get; [UsedImplicitly] set; }
|
|
||||||
|
|
||||||
public abstract string Name { get; }
|
|
||||||
|
|
||||||
protected override void RegisterServices(ContainerBuilder builder)
|
|
||||||
{
|
|
||||||
builder.RegisterInstance(this).As<IServiceCommand>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task Process(ILifetimeScope container)
|
|
||||||
{
|
|
||||||
var repoUpdater = container.Resolve<IRepoUpdater>();
|
|
||||||
var migration = container.Resolve<IMigrationExecutor>();
|
|
||||||
|
|
||||||
Logger.Debug("Recyclarr Version: {Version}", GitVersionInformation.InformationalVersion);
|
|
||||||
|
|
||||||
// Will throw if migration is required, otherwise just a warning is issued.
|
|
||||||
migration.CheckNeededMigrations();
|
|
||||||
|
|
||||||
await repoUpdater.UpdateRepo();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,140 +0,0 @@
|
|||||||
using Autofac;
|
|
||||||
using CliFx.Attributes;
|
|
||||||
using CliFx.Exceptions;
|
|
||||||
using CliFx.Infrastructure;
|
|
||||||
using JetBrains.Annotations;
|
|
||||||
using Recyclarr.Cli.Config;
|
|
||||||
using Recyclarr.TrashLib.Config.Services;
|
|
||||||
using Recyclarr.TrashLib.Http;
|
|
||||||
using Recyclarr.TrashLib.Services.CustomFormat;
|
|
||||||
using Recyclarr.TrashLib.Services.QualitySize;
|
|
||||||
using Recyclarr.TrashLib.Services.Sonarr;
|
|
||||||
using Recyclarr.TrashLib.Services.Sonarr.Config;
|
|
||||||
using Recyclarr.TrashLib.Services.Sonarr.ReleaseProfile;
|
|
||||||
using Recyclarr.TrashLib.Services.Sonarr.ReleaseProfile.Guide;
|
|
||||||
using Serilog;
|
|
||||||
|
|
||||||
namespace Recyclarr.Cli.Command;
|
|
||||||
|
|
||||||
[Command("sonarr", Description = "Perform operations on a Sonarr instance")]
|
|
||||||
[UsedImplicitly]
|
|
||||||
public class SonarrCommand : ServiceCommand
|
|
||||||
{
|
|
||||||
// ReSharper disable MemberCanBePrivate.Global
|
|
||||||
|
|
||||||
[CommandOption("list-release-profiles", Description =
|
|
||||||
"List available release profiles from the guide in YAML format.")]
|
|
||||||
public bool ListReleaseProfiles { get; [UsedImplicitly] set; }
|
|
||||||
|
|
||||||
// The default value is "empty" because I need to know when the user specifies the option but no value with it.
|
|
||||||
// Discussed here: https://github.com/Tyrrrz/CliFx/discussions/128#discussioncomment-2647015
|
|
||||||
[CommandOption("list-terms", Description =
|
|
||||||
"For the given Release Profile Trash ID, list terms in it that can be filtered in YAML format. " +
|
|
||||||
"Note that not every release profile has terms that may be filtered.")]
|
|
||||||
public string? ListTerms { get; [UsedImplicitly] set; } = "empty";
|
|
||||||
|
|
||||||
[CommandOption("list-qualities", Description =
|
|
||||||
"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; }
|
|
||||||
|
|
||||||
// ReSharper restore MemberCanBePrivate.Global
|
|
||||||
|
|
||||||
public override string Name => "Sonarr";
|
|
||||||
|
|
||||||
public override async Task Process(ILifetimeScope container)
|
|
||||||
{
|
|
||||||
await base.Process(container);
|
|
||||||
|
|
||||||
var lister = container.Resolve<ISonarrGuideDataLister>();
|
|
||||||
var log = container.Resolve<ILogger>();
|
|
||||||
var guideService = container.Resolve<ISonarrGuideService>();
|
|
||||||
var console = container.Resolve<IConsole>();
|
|
||||||
|
|
||||||
if (ListReleaseProfiles)
|
|
||||||
{
|
|
||||||
lister.ListReleaseProfiles();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ListQualities)
|
|
||||||
{
|
|
||||||
lister.ListQualities();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ListCustomFormats)
|
|
||||||
{
|
|
||||||
lister.ListCustomFormats();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ListTerms != "empty")
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(ListTerms))
|
|
||||||
{
|
|
||||||
lister.ListTerms(ListTerms);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new CommandException(
|
|
||||||
"The --list-terms option was specified without a Release Profile Trash ID specified");
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var configFinder = container.Resolve<IConfigurationFinder>();
|
|
||||||
var configLoader = container.Resolve<IConfigurationLoader<SonarrConfiguration>>();
|
|
||||||
foreach (var config in configLoader.LoadMany(configFinder.GetConfigFiles(Config), "sonarr"))
|
|
||||||
{
|
|
||||||
await using var scope = container.BeginLifetimeScope(builder =>
|
|
||||||
{
|
|
||||||
builder.RegisterInstance(config).As<IServiceConfiguration>();
|
|
||||||
});
|
|
||||||
|
|
||||||
var serverName = Name;
|
|
||||||
var instanceName = config.Name ?? FlurlLogging.SanitizeUrl(config.BaseUrl);
|
|
||||||
|
|
||||||
await console.Output.WriteLineAsync($@"
|
|
||||||
===========================================
|
|
||||||
Processing {serverName} Server: [{instanceName}]
|
|
||||||
===========================================
|
|
||||||
");
|
|
||||||
|
|
||||||
log.Debug("Processing {Server} server {Name}", serverName, instanceName);
|
|
||||||
|
|
||||||
var validator = scope.Resolve<ConfigValidationExecutor>();
|
|
||||||
if (!validator.Validate(config))
|
|
||||||
{
|
|
||||||
log.Error("Due to validation failure, this instance will be skipped");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReSharper disable InvertIf
|
|
||||||
|
|
||||||
if (config.ReleaseProfiles.Count > 0)
|
|
||||||
{
|
|
||||||
var updater = scope.Resolve<IReleaseProfileUpdater>();
|
|
||||||
await updater.Process(Preview, config);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.QualityDefinition != null)
|
|
||||||
{
|
|
||||||
var updater = scope.Resolve<IQualitySizeUpdater>();
|
|
||||||
await updater.Process(Preview, config.QualityDefinition, guideService);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.CustomFormats.Count > 0)
|
|
||||||
{
|
|
||||||
var updater = scope.Resolve<ICustomFormatUpdater>();
|
|
||||||
await updater.Process(Preview, config.CustomFormats, guideService);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReSharper restore InvertIf
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,62 +0,0 @@
|
|||||||
using System.IO.Abstractions;
|
|
||||||
using CliFx.Exceptions;
|
|
||||||
using Recyclarr.TrashLib.Startup;
|
|
||||||
using Serilog;
|
|
||||||
|
|
||||||
namespace Recyclarr.Cli.Config;
|
|
||||||
|
|
||||||
public class ConfigurationFinder : IConfigurationFinder
|
|
||||||
{
|
|
||||||
private readonly IAppPaths _paths;
|
|
||||||
private readonly IFileSystem _fs;
|
|
||||||
private readonly ILogger _log;
|
|
||||||
|
|
||||||
public ConfigurationFinder(IAppPaths paths, IFileSystem fs, ILogger log)
|
|
||||||
{
|
|
||||||
_paths = paths;
|
|
||||||
_fs = fs;
|
|
||||||
_log = log;
|
|
||||||
}
|
|
||||||
|
|
||||||
private IReadOnlyCollection<string> FindDefaultConfigFiles()
|
|
||||||
{
|
|
||||||
var configs = new List<string>();
|
|
||||||
|
|
||||||
if (_paths.ConfigsDirectory.Exists)
|
|
||||||
{
|
|
||||||
configs.AddRange(_paths.ConfigsDirectory.EnumerateFiles("*.yml").Select(x => x.FullName));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_paths.ConfigPath.Exists)
|
|
||||||
{
|
|
||||||
configs.Add(_paths.ConfigPath.FullName);
|
|
||||||
}
|
|
||||||
|
|
||||||
return configs;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IReadOnlyCollection<string> GetConfigFiles(IReadOnlyCollection<string>? configs)
|
|
||||||
{
|
|
||||||
if (configs is null || !configs.Any())
|
|
||||||
{
|
|
||||||
configs = FindDefaultConfigFiles();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var split = configs.ToLookup(x => _fs.File.Exists(x));
|
|
||||||
foreach (var nonExistentConfig in split[false])
|
|
||||||
{
|
|
||||||
_log.Warning("Configuration file does not exist {File}", nonExistentConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
configs = split[true].ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (configs.Count == 0)
|
|
||||||
{
|
|
||||||
throw new CommandException("No configuration YAML files found");
|
|
||||||
}
|
|
||||||
|
|
||||||
return configs;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
namespace Recyclarr.Cli.Config;
|
|
||||||
|
|
||||||
public interface IConfigurationFinder
|
|
||||||
{
|
|
||||||
IReadOnlyCollection<string> GetConfigFiles(IReadOnlyCollection<string>? configs);
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
using Recyclarr.TrashLib.Config.Services;
|
|
||||||
|
|
||||||
namespace Recyclarr.Cli.Config;
|
|
||||||
|
|
||||||
public interface IConfigurationLoader<T>
|
|
||||||
where T : IServiceConfiguration
|
|
||||||
{
|
|
||||||
ICollection<T> LoadMany(IEnumerable<string> configFiles, string configSection);
|
|
||||||
ICollection<T> Load(string file, string configSection);
|
|
||||||
ICollection<T> LoadFromStream(TextReader stream, string requestedSection);
|
|
||||||
}
|
|
@ -0,0 +1,37 @@
|
|||||||
|
using Recyclarr.Cli.Console.Commands;
|
||||||
|
using Spectre.Console.Cli;
|
||||||
|
|
||||||
|
namespace Recyclarr.Cli.Console;
|
||||||
|
|
||||||
|
public static class CliSetup
|
||||||
|
{
|
||||||
|
public static void Commands(IConfigurator cli)
|
||||||
|
{
|
||||||
|
cli.AddCommand<SyncCommand>("sync")
|
||||||
|
.WithExample("sync", "radarr", "--instance", "movies")
|
||||||
|
.WithExample("sync", "-i", "instance1", "-i", "instance2")
|
||||||
|
.WithExample("sync", "sonarr", "--preview");
|
||||||
|
|
||||||
|
cli.AddCommand<MigrateCommand>("migrate");
|
||||||
|
|
||||||
|
cli.AddBranch("list", list =>
|
||||||
|
{
|
||||||
|
list.SetDescription("List information from the guide");
|
||||||
|
list.AddCommand<ListCustomFormatsCommand>("custom-formats");
|
||||||
|
list.AddCommand<ListReleaseProfilesCommand>("release-profiles");
|
||||||
|
list.AddCommand<ListQualitiesCommand>("qualities");
|
||||||
|
});
|
||||||
|
|
||||||
|
cli.AddBranch("config", config =>
|
||||||
|
{
|
||||||
|
config.SetDescription("Operations for configuration files");
|
||||||
|
config.AddCommand<ConfigCreateCommand>("create");
|
||||||
|
});
|
||||||
|
|
||||||
|
// LEGACY / DEPRECATED SUBCOMMANDS
|
||||||
|
cli.AddCommand<RadarrCommand>("radarr");
|
||||||
|
cli.AddCommand<SonarrCommand>("sonarr");
|
||||||
|
cli.AddCommand<ConfigCreateCommand>("create-config")
|
||||||
|
.WithDescription("OBSOLETE: Use `config create` instead");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
using Spectre.Console.Cli;
|
||||||
|
|
||||||
|
namespace Recyclarr.Cli.Console;
|
||||||
|
|
||||||
|
public static class CommandConfiguratorExtensions
|
||||||
|
{
|
||||||
|
public static ICommandConfigurator WithExample(this ICommandConfigurator cli, params string[] args)
|
||||||
|
{
|
||||||
|
return cli.WithExample(args);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,57 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using Recyclarr.Cli.Console.Commands.Shared;
|
||||||
|
using Recyclarr.TrashLib.ExceptionTypes;
|
||||||
|
using Recyclarr.TrashLib.Services.Processors;
|
||||||
|
using Serilog;
|
||||||
|
using Spectre.Console.Cli;
|
||||||
|
|
||||||
|
namespace Recyclarr.Cli.Console.Commands;
|
||||||
|
|
||||||
|
[UsedImplicitly]
|
||||||
|
[Description("Create a starter configuration file.")]
|
||||||
|
public class ConfigCreateCommand : AsyncCommand<ConfigCreateCommand.CliSettings>
|
||||||
|
{
|
||||||
|
private readonly IConfigCreationProcessor _processor;
|
||||||
|
private readonly ILogger _log;
|
||||||
|
|
||||||
|
[UsedImplicitly]
|
||||||
|
[SuppressMessage("Design", "CA1034:Nested types should not be visible")]
|
||||||
|
public class CliSettings : BaseCommandSettings
|
||||||
|
{
|
||||||
|
[CommandOption("-p|--path")]
|
||||||
|
[Description("Path to where the configuration file should be created.")]
|
||||||
|
[UsedImplicitly(ImplicitUseKindFlags.Assign)]
|
||||||
|
public string? Path { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public ConfigCreateCommand(ILogger log, IConfigCreationProcessor processor)
|
||||||
|
{
|
||||||
|
_processor = processor;
|
||||||
|
_log = log;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<int> ExecuteAsync(CommandContext context, CliSettings settings)
|
||||||
|
{
|
||||||
|
if (context.Name == "create-config")
|
||||||
|
{
|
||||||
|
_log.Warning("The `create-config` subcommand is DEPRECATED -- Use `config create` instead!");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _processor.Process(settings.Path);
|
||||||
|
}
|
||||||
|
catch (FileExistsException e)
|
||||||
|
{
|
||||||
|
_log.Error(
|
||||||
|
"The file {ConfigFile} already exists. Please choose another path or " +
|
||||||
|
"delete/move the existing file and run this command again", e.AttemptedPath);
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Autofac.Features.Indexed;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using Recyclarr.Cli.Console.Commands.Shared;
|
||||||
|
using Recyclarr.Cli.Console.Helpers;
|
||||||
|
using Recyclarr.TrashLib.Config;
|
||||||
|
using Recyclarr.TrashLib.Services.Common;
|
||||||
|
using Spectre.Console.Cli;
|
||||||
|
|
||||||
|
#pragma warning disable CS8765
|
||||||
|
|
||||||
|
namespace Recyclarr.Cli.Console.Commands;
|
||||||
|
|
||||||
|
[UsedImplicitly]
|
||||||
|
[Description("List custom formats in the guide for a particular service.")]
|
||||||
|
internal class ListCustomFormatsCommand : Command<ListCustomFormatsCommand.CliSettings>
|
||||||
|
{
|
||||||
|
private readonly IGuideDataLister _lister;
|
||||||
|
private readonly IIndex<SupportedServices, IGuideService> _guideService;
|
||||||
|
|
||||||
|
[UsedImplicitly]
|
||||||
|
[SuppressMessage("Design", "CA1034:Nested types should not be visible")]
|
||||||
|
public class CliSettings : BaseCommandSettings
|
||||||
|
{
|
||||||
|
[CommandArgument(0, "<service>")]
|
||||||
|
[EnumDescription<SupportedServices>("The service to obtain information about.")]
|
||||||
|
[UsedImplicitly(ImplicitUseKindFlags.Assign)]
|
||||||
|
public required SupportedServices Service { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public ListCustomFormatsCommand(
|
||||||
|
IGuideDataLister lister,
|
||||||
|
IIndex<SupportedServices, IGuideService> guideService)
|
||||||
|
{
|
||||||
|
_lister = lister;
|
||||||
|
_guideService = guideService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int Execute(CommandContext context, CliSettings settings)
|
||||||
|
{
|
||||||
|
var guideService = _guideService[settings.Service];
|
||||||
|
_lister.ListCustomFormats(guideService.GetCustomFormatData());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Autofac.Features.Indexed;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using Recyclarr.Cli.Console.Commands.Shared;
|
||||||
|
using Recyclarr.Cli.Console.Helpers;
|
||||||
|
using Recyclarr.TrashLib.Config;
|
||||||
|
using Recyclarr.TrashLib.Services.Common;
|
||||||
|
using Spectre.Console.Cli;
|
||||||
|
|
||||||
|
namespace Recyclarr.Cli.Console.Commands;
|
||||||
|
|
||||||
|
#pragma warning disable CS8765
|
||||||
|
[UsedImplicitly]
|
||||||
|
[Description("List quality definitions in the guide for a particular service.")]
|
||||||
|
internal class ListQualitiesCommand : Command<ListQualitiesCommand.CliSettings>
|
||||||
|
{
|
||||||
|
private readonly IGuideDataLister _lister;
|
||||||
|
private readonly IIndex<SupportedServices, IGuideService> _guideService;
|
||||||
|
|
||||||
|
[UsedImplicitly]
|
||||||
|
[SuppressMessage("Design", "CA1034:Nested types should not be visible")]
|
||||||
|
public class CliSettings : BaseCommandSettings
|
||||||
|
{
|
||||||
|
[CommandArgument(0, "<service>")]
|
||||||
|
[EnumDescription<SupportedServices>("The service to obtain information about.")]
|
||||||
|
[UsedImplicitly(ImplicitUseKindFlags.Assign)]
|
||||||
|
public required SupportedServices Service { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public ListQualitiesCommand(
|
||||||
|
IGuideDataLister lister,
|
||||||
|
IIndex<SupportedServices, IGuideService> guideService)
|
||||||
|
{
|
||||||
|
_lister = lister;
|
||||||
|
_guideService = guideService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int Execute(CommandContext context, CliSettings settings)
|
||||||
|
{
|
||||||
|
var guideService = _guideService[settings.Service];
|
||||||
|
_lister.ListQualities(guideService.GetQualities());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using Recyclarr.Cli.Console.Commands.Shared;
|
||||||
|
using Recyclarr.TrashLib.Services.Sonarr;
|
||||||
|
using Serilog;
|
||||||
|
using Spectre.Console.Cli;
|
||||||
|
|
||||||
|
#pragma warning disable CS8765
|
||||||
|
|
||||||
|
namespace Recyclarr.Cli.Console.Commands;
|
||||||
|
|
||||||
|
[UsedImplicitly]
|
||||||
|
[Description("List Sonarr release profiles in the guide for a particular service.")]
|
||||||
|
internal class ListReleaseProfilesCommand : Command<ListReleaseProfilesCommand.CliSettings>
|
||||||
|
{
|
||||||
|
private readonly ILogger _log;
|
||||||
|
private readonly ISonarrGuideDataLister _lister;
|
||||||
|
|
||||||
|
[UsedImplicitly]
|
||||||
|
[SuppressMessage("Design", "CA1034:Nested types should not be visible")]
|
||||||
|
public class CliSettings : BaseCommandSettings
|
||||||
|
{
|
||||||
|
[CommandOption("--terms")]
|
||||||
|
[Description(
|
||||||
|
"For the given Release Profile Trash ID, list terms in it that can be filtered in YAML format. " +
|
||||||
|
"Note that not every release profile has terms that may be filtered.")]
|
||||||
|
[UsedImplicitly(ImplicitUseKindFlags.Assign)]
|
||||||
|
public string? ListTerms { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public ListReleaseProfilesCommand(
|
||||||
|
ILogger log,
|
||||||
|
ISonarrGuideDataLister lister)
|
||||||
|
{
|
||||||
|
_log = log;
|
||||||
|
_lister = lister;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int Execute(CommandContext context, CliSettings settings)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (settings.ListTerms is not null)
|
||||||
|
{
|
||||||
|
// Ignore nullability of ListTerms since the Settings.Validate() method will check for null/empty.
|
||||||
|
_lister.ListTerms(settings.ListTerms!);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_lister.ListReleaseProfiles();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (ArgumentException e)
|
||||||
|
{
|
||||||
|
_log.Error(e, "Error");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Text;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using Recyclarr.Cli.Console.Commands.Shared;
|
||||||
|
using Recyclarr.Cli.Migration;
|
||||||
|
using Spectre.Console;
|
||||||
|
using Spectre.Console.Cli;
|
||||||
|
|
||||||
|
#pragma warning disable CS8765
|
||||||
|
|
||||||
|
namespace Recyclarr.Cli.Console.Commands;
|
||||||
|
|
||||||
|
[UsedImplicitly]
|
||||||
|
[Description("Perform migration steps that may be needed between versions")]
|
||||||
|
public class MigrateCommand : Command<MigrateCommand.CliSettings>
|
||||||
|
{
|
||||||
|
private readonly IMigrationExecutor _migration;
|
||||||
|
private readonly IAnsiConsole _console;
|
||||||
|
|
||||||
|
[UsedImplicitly]
|
||||||
|
[SuppressMessage("Design", "CA1034:Nested types should not be visible")]
|
||||||
|
public class CliSettings : ServiceCommandSettings
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public MigrateCommand(
|
||||||
|
IAnsiConsole console,
|
||||||
|
IMigrationExecutor migration)
|
||||||
|
{
|
||||||
|
_console = console;
|
||||||
|
_migration = migration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int Execute(CommandContext context, CliSettings settings)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_migration.PerformAllMigrationSteps(settings.Debug);
|
||||||
|
}
|
||||||
|
catch (MigrationException e)
|
||||||
|
{
|
||||||
|
var msg = new StringBuilder();
|
||||||
|
msg.AppendLine("Fatal exception during migration step. Details are below.\n");
|
||||||
|
msg.AppendLine($"Step That Failed: {e.OperationDescription}");
|
||||||
|
msg.AppendLine($"Failure Reason: {e.OriginalException.Message}");
|
||||||
|
|
||||||
|
// ReSharper disable once InvertIf
|
||||||
|
if (e.Remediation.Any())
|
||||||
|
{
|
||||||
|
msg.AppendLine("\nPossible remediation steps:");
|
||||||
|
foreach (var remedy in e.Remediation)
|
||||||
|
{
|
||||||
|
msg.AppendLine($" - {remedy}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_console.Write(msg.ToString());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
catch (RequiredMigrationException ex)
|
||||||
|
{
|
||||||
|
_console.WriteLine($"ERROR: {ex.Message}");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,90 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.IO.Abstractions;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using Recyclarr.Cli.Console.Commands.Shared;
|
||||||
|
using Recyclarr.Cli.Console.Helpers;
|
||||||
|
using Recyclarr.Cli.Migration;
|
||||||
|
using Recyclarr.TrashLib.Config;
|
||||||
|
using Recyclarr.TrashLib.Services.Processors;
|
||||||
|
using Recyclarr.TrashLib.Services.Radarr;
|
||||||
|
using Serilog;
|
||||||
|
using Spectre.Console.Cli;
|
||||||
|
|
||||||
|
namespace Recyclarr.Cli.Console.Commands;
|
||||||
|
|
||||||
|
[UsedImplicitly]
|
||||||
|
[Description("OBSOLETE: Use `sync radarr` instead")]
|
||||||
|
internal class RadarrCommand : AsyncCommand<RadarrCommand.CliSettings>
|
||||||
|
{
|
||||||
|
private readonly ILogger _log;
|
||||||
|
private readonly IRadarrGuideDataLister _lister;
|
||||||
|
private readonly IMigrationExecutor _migration;
|
||||||
|
private readonly ISyncProcessor _syncProcessor;
|
||||||
|
|
||||||
|
[UsedImplicitly]
|
||||||
|
[SuppressMessage("Design", "CA1034:Nested types should not be visible")]
|
||||||
|
public class CliSettings : ServiceCommandSettings, ISyncSettings
|
||||||
|
{
|
||||||
|
public SupportedServices? Service => SupportedServices.Radarr;
|
||||||
|
public IReadOnlyCollection<string> Instances { get; } = Array.Empty<string>();
|
||||||
|
|
||||||
|
[CommandOption("-p|--preview")]
|
||||||
|
[Description("Only display the processed markdown results without making any API calls.")]
|
||||||
|
[UsedImplicitly(ImplicitUseKindFlags.Assign)]
|
||||||
|
public bool Preview { get; init; }
|
||||||
|
|
||||||
|
[CommandOption("-c|--config")]
|
||||||
|
[Description(
|
||||||
|
"One or more YAML config files to use. All configs will be used and settings are additive. " +
|
||||||
|
"If not specified, the script will look for `recyclarr.yml` in the same directory as the executable.")]
|
||||||
|
[UsedImplicitly(ImplicitUseKindFlags.Assign)]
|
||||||
|
[TypeConverter(typeof(FileInfoConverter))]
|
||||||
|
public IFileInfo[] ConfigsOption { get; init; } = Array.Empty<IFileInfo>();
|
||||||
|
public IReadOnlyCollection<IFileInfo> Configs => ConfigsOption;
|
||||||
|
|
||||||
|
[CommandOption("--list-custom-formats")]
|
||||||
|
[Description("List available custom formats from the guide in YAML format.")]
|
||||||
|
[UsedImplicitly(ImplicitUseKindFlags.Assign)]
|
||||||
|
public bool ListCustomFormats { get; init; }
|
||||||
|
|
||||||
|
[CommandOption("--list-qualities")]
|
||||||
|
[Description("List available quality definition types from the guide.")]
|
||||||
|
[UsedImplicitly(ImplicitUseKindFlags.Assign)]
|
||||||
|
public bool ListQualities { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public RadarrCommand(
|
||||||
|
ILogger log,
|
||||||
|
IRadarrGuideDataLister lister,
|
||||||
|
IMigrationExecutor migration,
|
||||||
|
ISyncProcessor syncProcessor)
|
||||||
|
{
|
||||||
|
_log = log;
|
||||||
|
_lister = lister;
|
||||||
|
_migration = migration;
|
||||||
|
_syncProcessor = syncProcessor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<int> ExecuteAsync(CommandContext context, CliSettings settings)
|
||||||
|
{
|
||||||
|
_log.Warning("The `radarr` subcommand is DEPRECATED -- Use `sync` instead!");
|
||||||
|
|
||||||
|
if (settings.ListCustomFormats)
|
||||||
|
{
|
||||||
|
_lister.ListCustomFormats();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.ListQualities)
|
||||||
|
{
|
||||||
|
_lister.ListQualities();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Will throw if migration is required, otherwise just a warning is issued.
|
||||||
|
_migration.CheckNeededMigrations();
|
||||||
|
|
||||||
|
return (int) await _syncProcessor.ProcessConfigs(settings);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using Spectre.Console.Cli;
|
||||||
|
|
||||||
|
namespace Recyclarr.Cli.Console.Commands.Shared;
|
||||||
|
|
||||||
|
public class BaseCommandSettings : CommandSettings
|
||||||
|
{
|
||||||
|
[CommandOption("-d|--debug")]
|
||||||
|
[Description("Show debug logs in console output.")]
|
||||||
|
[UsedImplicitly(ImplicitUseKindFlags.Assign)]
|
||||||
|
public bool Debug { get; init; }
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using Spectre.Console.Cli;
|
||||||
|
|
||||||
|
namespace Recyclarr.Cli.Console.Commands.Shared;
|
||||||
|
|
||||||
|
public class ServiceCommandSettings : BaseCommandSettings
|
||||||
|
{
|
||||||
|
[CommandOption("--app-data")]
|
||||||
|
[Description("Custom path to the application data directory")]
|
||||||
|
[UsedImplicitly(ImplicitUseKindFlags.Assign)]
|
||||||
|
public string? AppData { get; init; }
|
||||||
|
}
|
@ -0,0 +1,129 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.IO.Abstractions;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using Recyclarr.Cli.Console.Commands.Shared;
|
||||||
|
using Recyclarr.Cli.Console.Helpers;
|
||||||
|
using Recyclarr.Cli.Migration;
|
||||||
|
using Recyclarr.TrashLib.Config;
|
||||||
|
using Recyclarr.TrashLib.Services.Processors;
|
||||||
|
using Recyclarr.TrashLib.Services.Sonarr;
|
||||||
|
using Serilog;
|
||||||
|
using Spectre.Console;
|
||||||
|
using Spectre.Console.Cli;
|
||||||
|
|
||||||
|
namespace Recyclarr.Cli.Console.Commands;
|
||||||
|
|
||||||
|
[UsedImplicitly]
|
||||||
|
[Description("OBSOLETE: Use `sync sonarr` instead")]
|
||||||
|
internal class SonarrCommand : AsyncCommand<SonarrCommand.CliSettings>
|
||||||
|
{
|
||||||
|
private readonly ILogger _log;
|
||||||
|
private readonly ISonarrGuideDataLister _lister;
|
||||||
|
private readonly IMigrationExecutor _migration;
|
||||||
|
private readonly ISyncProcessor _syncProcessor;
|
||||||
|
|
||||||
|
[UsedImplicitly]
|
||||||
|
[SuppressMessage("Design", "CA1034:Nested types should not be visible")]
|
||||||
|
public class CliSettings : ServiceCommandSettings, ISyncSettings
|
||||||
|
{
|
||||||
|
public SupportedServices? Service => SupportedServices.Sonarr;
|
||||||
|
public IReadOnlyCollection<string> Instances { get; } = Array.Empty<string>();
|
||||||
|
|
||||||
|
[CommandOption("-p|--preview")]
|
||||||
|
[Description("Only display the processed markdown results without making any API calls.")]
|
||||||
|
[UsedImplicitly(ImplicitUseKindFlags.Assign)]
|
||||||
|
public bool Preview { get; init; }
|
||||||
|
|
||||||
|
[CommandOption("-c|--config")]
|
||||||
|
[Description(
|
||||||
|
"One or more YAML config files to use. All configs will be used and settings are additive. " +
|
||||||
|
"If not specified, the script will look for `recyclarr.yml` in the same directory as the executable.")]
|
||||||
|
[UsedImplicitly(ImplicitUseKindFlags.Assign)]
|
||||||
|
[TypeConverter(typeof(FileInfoConverter))]
|
||||||
|
public IFileInfo[] ConfigsOption { get; init; } = Array.Empty<IFileInfo>();
|
||||||
|
public IReadOnlyCollection<IFileInfo> Configs => ConfigsOption;
|
||||||
|
|
||||||
|
[CommandOption("--list-custom-formats")]
|
||||||
|
[Description("List available custom formats from the guide in YAML format.")]
|
||||||
|
[UsedImplicitly(ImplicitUseKindFlags.Assign)]
|
||||||
|
public bool ListCustomFormats { get; init; }
|
||||||
|
|
||||||
|
[CommandOption("--list-qualities")]
|
||||||
|
[Description("List available quality definition types from the guide.")]
|
||||||
|
[UsedImplicitly(ImplicitUseKindFlags.Assign)]
|
||||||
|
public bool ListQualities { get; init; }
|
||||||
|
|
||||||
|
[CommandOption("--list-release-profiles")]
|
||||||
|
[Description("List available release profiles from the guide in YAML format.")]
|
||||||
|
[UsedImplicitly(ImplicitUseKindFlags.Assign)]
|
||||||
|
public bool ListReleaseProfiles { get; init; }
|
||||||
|
|
||||||
|
// The default value is "empty" because I need to know when the user specifies the option but no value with it.
|
||||||
|
// Discussed here: https://github.com/Tyrrrz/CliFx/discussions/128#discussioncomment-2647015
|
||||||
|
[CommandOption("--list-terms")]
|
||||||
|
[UsedImplicitly(ImplicitUseKindFlags.Assign)]
|
||||||
|
[Description(
|
||||||
|
"For the given Release Profile Trash ID, list terms in it that can be filtered in YAML format. " +
|
||||||
|
"Note that not every release profile has terms that may be filtered.")]
|
||||||
|
public string? ListTerms { get; init; } = "empty";
|
||||||
|
|
||||||
|
public override ValidationResult Validate()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(ListTerms))
|
||||||
|
{
|
||||||
|
return ValidationResult.Error(
|
||||||
|
"The --list-terms option was specified without a Release Profile Trash ID specified");
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.Validate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public SonarrCommand(
|
||||||
|
ILogger log,
|
||||||
|
ISonarrGuideDataLister lister,
|
||||||
|
IMigrationExecutor migration,
|
||||||
|
ISyncProcessor syncProcessor)
|
||||||
|
{
|
||||||
|
_log = log;
|
||||||
|
_lister = lister;
|
||||||
|
_migration = migration;
|
||||||
|
_syncProcessor = syncProcessor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<int> ExecuteAsync(CommandContext context, CliSettings settings)
|
||||||
|
{
|
||||||
|
_log.Warning("The `sonarr` subcommand is DEPRECATED -- Use `sync` instead!");
|
||||||
|
|
||||||
|
if (settings.ListCustomFormats)
|
||||||
|
{
|
||||||
|
_lister.ListCustomFormats();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.ListQualities)
|
||||||
|
{
|
||||||
|
_lister.ListQualities();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.ListReleaseProfiles)
|
||||||
|
{
|
||||||
|
_lister.ListReleaseProfiles();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.ListTerms != "empty")
|
||||||
|
{
|
||||||
|
// Ignore nullability of ListTerms since the Settings.Validate() method will check for null/empty.
|
||||||
|
_lister.ListTerms(settings.ListTerms!);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Will throw if migration is required, otherwise just a warning is issued.
|
||||||
|
_migration.CheckNeededMigrations();
|
||||||
|
|
||||||
|
return (int) await _syncProcessor.ProcessConfigs(settings);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.IO.Abstractions;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using Recyclarr.Cli.Console.Commands.Shared;
|
||||||
|
using Recyclarr.Cli.Console.Helpers;
|
||||||
|
using Recyclarr.Cli.Migration;
|
||||||
|
using Recyclarr.TrashLib.Config;
|
||||||
|
using Recyclarr.TrashLib.Services.Processors;
|
||||||
|
using Spectre.Console.Cli;
|
||||||
|
|
||||||
|
namespace Recyclarr.Cli.Console.Commands;
|
||||||
|
|
||||||
|
[Description("Sync the guide to services")]
|
||||||
|
[UsedImplicitly]
|
||||||
|
public class SyncCommand : AsyncCommand<SyncCommand.CliSettings>
|
||||||
|
{
|
||||||
|
private readonly IMigrationExecutor _migration;
|
||||||
|
private readonly ISyncProcessor _syncProcessor;
|
||||||
|
|
||||||
|
[UsedImplicitly]
|
||||||
|
[SuppressMessage("Design", "CA1034:Nested types should not be visible")]
|
||||||
|
[SuppressMessage("Performance", "CA1819:Properties should not return arrays",
|
||||||
|
Justification = "Spectre.Console requires it")]
|
||||||
|
public class CliSettings : ServiceCommandSettings, ISyncSettings
|
||||||
|
{
|
||||||
|
[CommandArgument(0, "[service]")]
|
||||||
|
[EnumDescription<SupportedServices>("The service to sync. If not specified, all services are synced.")]
|
||||||
|
[UsedImplicitly(ImplicitUseKindFlags.Assign)]
|
||||||
|
public SupportedServices? Service { get; init; }
|
||||||
|
|
||||||
|
[CommandOption("-c|--config")]
|
||||||
|
[Description("One or more YAML configuration files to load & use.")]
|
||||||
|
[TypeConverter(typeof(FileInfoConverter))]
|
||||||
|
[UsedImplicitly(ImplicitUseKindFlags.Assign)]
|
||||||
|
public IFileInfo[] ConfigsOption { get; init; } = Array.Empty<IFileInfo>();
|
||||||
|
public IReadOnlyCollection<IFileInfo> Configs => ConfigsOption;
|
||||||
|
|
||||||
|
[CommandOption("-p|--preview")]
|
||||||
|
[Description("Perform a dry run: preview the results without syncing.")]
|
||||||
|
[UsedImplicitly(ImplicitUseKindFlags.Assign)]
|
||||||
|
public bool Preview { get; init; }
|
||||||
|
|
||||||
|
[CommandOption("-i|--instance")]
|
||||||
|
[Description("One or more instance names to sync. If not specified, all instances will be synced.")]
|
||||||
|
[UsedImplicitly(ImplicitUseKindFlags.Assign)]
|
||||||
|
public string[] InstancesOption { get; init; } = Array.Empty<string>();
|
||||||
|
public IReadOnlyCollection<string> Instances => InstancesOption;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SyncCommand(
|
||||||
|
IMigrationExecutor migration,
|
||||||
|
ISyncProcessor syncProcessor)
|
||||||
|
{
|
||||||
|
_migration = migration;
|
||||||
|
_syncProcessor = syncProcessor;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SuppressMessage("Design", "CA1031:Do not catch general exception types")]
|
||||||
|
public override async Task<int> ExecuteAsync(CommandContext context, CliSettings settings)
|
||||||
|
{
|
||||||
|
// Will throw if migration is required, otherwise just a warning is issued.
|
||||||
|
_migration.CheckNeededMigrations();
|
||||||
|
|
||||||
|
return (int) await _syncProcessor.ProcessConfigs(settings);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
namespace Recyclarr.Cli.Console.Helpers;
|
||||||
|
|
||||||
|
public class AppDataPathProvider
|
||||||
|
{
|
||||||
|
public string? AppDataPath { get; set; }
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Recyclarr.Cli.Console.Helpers;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
|
public sealed class EnumDescriptionAttribute<TEnum> : DescriptionAttribute
|
||||||
|
where TEnum : Enum
|
||||||
|
{
|
||||||
|
public override string Description { get; }
|
||||||
|
|
||||||
|
public EnumDescriptionAttribute(string description)
|
||||||
|
{
|
||||||
|
var enumNames = Enum.GetNames(typeof(TEnum))
|
||||||
|
.Select(x => x.ToLowerInvariant());
|
||||||
|
|
||||||
|
var str = new StringBuilder(description.Trim());
|
||||||
|
str.Append($" (Valid Values: {string.Join(", ", enumNames)})");
|
||||||
|
Description = str.ToString();
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
using Recyclarr.TrashLib.Startup;
|
using Recyclarr.TrashLib.Startup;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
|
||||||
namespace Recyclarr.Cli.Command.Setup;
|
namespace Recyclarr.Cli.Console.Setup;
|
||||||
|
|
||||||
public class AppPathSetupTask : IBaseCommandSetupTask
|
public class AppPathSetupTask : IBaseCommandSetupTask
|
||||||
{
|
{
|
@ -1,4 +1,4 @@
|
|||||||
namespace Recyclarr.Cli.Command.Setup;
|
namespace Recyclarr.Cli.Console.Setup;
|
||||||
|
|
||||||
public interface IBaseCommandSetupTask
|
public interface IBaseCommandSetupTask
|
||||||
{
|
{
|
@ -0,0 +1,10 @@
|
|||||||
|
namespace Recyclarr.Cli.Migration;
|
||||||
|
|
||||||
|
public class RequiredMigrationException : Exception
|
||||||
|
{
|
||||||
|
public RequiredMigrationException()
|
||||||
|
: base("Some REQUIRED migrations did not pass")
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
@ -1,41 +1,97 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Text;
|
using System.Text.RegularExpressions;
|
||||||
using Autofac;
|
using Autofac;
|
||||||
using CliFx;
|
using MoreLinq;
|
||||||
|
using Recyclarr.Cli.Console;
|
||||||
|
using Recyclarr.Cli.Console.Helpers;
|
||||||
|
using Recyclarr.Cli.Console.Setup;
|
||||||
|
using Serilog;
|
||||||
|
using Serilog.Core;
|
||||||
|
using Spectre.Console;
|
||||||
|
using Spectre.Console.Cli;
|
||||||
|
|
||||||
namespace Recyclarr.Cli;
|
namespace Recyclarr.Cli;
|
||||||
|
|
||||||
internal static class Program
|
internal static partial class Program
|
||||||
{
|
{
|
||||||
private static string ExecutableName => Process.GetCurrentProcess().ProcessName;
|
private static ILifetimeScope? _scope;
|
||||||
|
private static IBaseCommandSetupTask[] _tasks = Array.Empty<IBaseCommandSetupTask>();
|
||||||
|
private static ILogger? _log;
|
||||||
|
|
||||||
public static async Task<int> Main()
|
[SuppressMessage("Design", "CA1031:Do not catch general exception types")]
|
||||||
|
public static int Main(string[] args)
|
||||||
{
|
{
|
||||||
var status = await new CliApplicationBuilder()
|
var builder = new ContainerBuilder();
|
||||||
.AddCommands(GetAllCommandTypes())
|
CompositionRoot.Setup(builder);
|
||||||
.SetExecutableName(ExecutableName)
|
|
||||||
.SetVersion(BuildVersion())
|
|
||||||
.Build()
|
|
||||||
.RunAsync();
|
|
||||||
|
|
||||||
return status;
|
var logLevelSwitch = new LoggingLevelSwitch();
|
||||||
}
|
builder.RegisterInstance(logLevelSwitch);
|
||||||
|
|
||||||
|
var appDataPathProvider = new AppDataPathProvider();
|
||||||
|
builder.RegisterInstance(appDataPathProvider);
|
||||||
|
|
||||||
|
var app = new CommandApp(new AutofacTypeRegistrar(builder, s => _scope = s));
|
||||||
|
app.Configure(config =>
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
config.PropagateExceptions();
|
||||||
|
config.ValidateExamples();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
config.Settings.PropagateExceptions = true;
|
||||||
|
config.SetApplicationName("recyclarr");
|
||||||
|
// config.SetApplicationVersion("v1.2.3");
|
||||||
|
|
||||||
private static IEnumerable<Type> GetAllCommandTypes()
|
var interceptor = new CliInterceptor(logLevelSwitch, appDataPathProvider);
|
||||||
|
interceptor.OnIntercepted.Subscribe(_ => OnAppInitialized());
|
||||||
|
config.SetInterceptor(interceptor);
|
||||||
|
|
||||||
|
CliSetup.Commands(config);
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = 1;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
result = app.Run(args);
|
||||||
|
}
|
||||||
|
catch (CommandRuntimeException ex)
|
||||||
|
{
|
||||||
|
var msg = CommandMessageRegex().Replace(ex.Message, "[gold1]$0[/]");
|
||||||
|
AnsiConsole.Markup($"[red]Error:[/] [white]{msg}[/]");
|
||||||
|
_log?.Debug(ex, "Command Exception");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AnsiConsole.WriteException(ex, ExceptionFormats.ShortenEverything);
|
||||||
|
_log?.Debug(ex, "Non-recoverable Exception");
|
||||||
|
}
|
||||||
|
finally
|
||||||
{
|
{
|
||||||
return typeof(Program).Assembly.GetTypes()
|
OnAppCleanup();
|
||||||
.Where(x => x.IsAssignableTo<ICommand>() && !x.IsAbstract);
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string BuildVersion()
|
private static void OnAppInitialized()
|
||||||
{
|
{
|
||||||
var builder = new StringBuilder($"v{GitVersionInformation.MajorMinorPatch}");
|
if (_scope is null)
|
||||||
var metadata = GitVersionInformation.FullBuildMetaData;
|
|
||||||
if (!string.IsNullOrEmpty(metadata))
|
|
||||||
{
|
{
|
||||||
builder.Append($" ({metadata})");
|
throw new InvalidProgramException("Composition root is not initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
return builder.ToString();
|
_log = _scope.Resolve<ILogger>();
|
||||||
|
_log.Debug("Recyclarr Version: {Version}", GitVersionInformation.InformationalVersion);
|
||||||
|
|
||||||
|
_tasks = _scope.Resolve<IOrderedEnumerable<IBaseCommandSetupTask>>().ToArray();
|
||||||
|
_tasks.ForEach(x => x.OnStart());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void OnAppCleanup()
|
||||||
|
{
|
||||||
|
_tasks.Reverse().ForEach(x => x.OnFinish());
|
||||||
|
}
|
||||||
|
|
||||||
|
[GeneratedRegex("'.*?'")]
|
||||||
|
private static partial Regex CommandMessageRegex();
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
using Autofac;
|
||||||
|
|
||||||
|
namespace Recyclarr.Common.Autofac;
|
||||||
|
|
||||||
|
public sealed class LifetimeScopedValue<T> : IDisposable
|
||||||
|
{
|
||||||
|
private readonly ILifetimeScope _scope;
|
||||||
|
|
||||||
|
public LifetimeScopedValue(ILifetimeScope scope, T value)
|
||||||
|
{
|
||||||
|
_scope = scope;
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public T Value { get; }
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_scope.Dispose();
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +1,26 @@
|
|||||||
|
using System.Reflection;
|
||||||
using Autofac;
|
using Autofac;
|
||||||
|
using Recyclarr.Common.FluentValidation;
|
||||||
|
using Module = Autofac.Module;
|
||||||
|
|
||||||
namespace Recyclarr.Common;
|
namespace Recyclarr.Common;
|
||||||
|
|
||||||
public class CommonAutofacModule : Module
|
public class CommonAutofacModule : Module
|
||||||
{
|
{
|
||||||
|
private readonly Assembly _rootAssembly;
|
||||||
|
|
||||||
|
public CommonAutofacModule(Assembly rootAssembly)
|
||||||
|
{
|
||||||
|
_rootAssembly = rootAssembly;
|
||||||
|
}
|
||||||
|
|
||||||
protected override void Load(ContainerBuilder builder)
|
protected override void Load(ContainerBuilder builder)
|
||||||
{
|
{
|
||||||
builder.RegisterType<DefaultEnvironment>().As<IEnvironment>();
|
builder.RegisterType<DefaultEnvironment>().As<IEnvironment>();
|
||||||
builder.RegisterType<FileUtilities>().As<IFileUtilities>();
|
builder.RegisterType<FileUtilities>().As<IFileUtilities>();
|
||||||
|
builder.RegisterType<ValidatorFactory>();
|
||||||
|
|
||||||
|
builder.Register(_ => new ResourceDataReader(_rootAssembly))
|
||||||
|
.As<IResourceDataReader>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
using Autofac;
|
||||||
|
|
||||||
|
namespace Recyclarr.Common.Extensions;
|
||||||
|
|
||||||
|
public static class AutofacExtensions
|
||||||
|
{
|
||||||
|
public static object ResolveGeneric(this ILifetimeScope scope, Type genericType, params Type[] genericArgs)
|
||||||
|
{
|
||||||
|
var type = genericType.MakeGenericType(genericArgs);
|
||||||
|
return scope.Resolve(type);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
|
||||||
|
namespace Recyclarr.Common.FluentValidation;
|
||||||
|
|
||||||
|
public interface IValidatorFactory
|
||||||
|
{
|
||||||
|
IValidator GetValidator(Type typeToValidate);
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
using Autofac;
|
||||||
|
using FluentValidation;
|
||||||
|
using Recyclarr.Common.Extensions;
|
||||||
|
|
||||||
|
namespace Recyclarr.Common.FluentValidation;
|
||||||
|
|
||||||
|
public class ValidatorFactory : IValidatorFactory
|
||||||
|
{
|
||||||
|
private readonly ILifetimeScope _scope;
|
||||||
|
|
||||||
|
public ValidatorFactory(ILifetimeScope scope)
|
||||||
|
{
|
||||||
|
_scope = scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IValidator GetValidator(Type typeToValidate)
|
||||||
|
{
|
||||||
|
return (IValidator) _scope.ResolveGeneric(typeof(IValidator<>), typeToValidate);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
namespace Recyclarr.Common;
|
||||||
|
|
||||||
|
public interface IResourceDataReader
|
||||||
|
{
|
||||||
|
string ReadData(string filename);
|
||||||
|
}
|
@ -1,65 +0,0 @@
|
|||||||
using System.Reactive.Linq;
|
|
||||||
using System.Reactive.Subjects;
|
|
||||||
using System.Text;
|
|
||||||
using CliFx.Infrastructure;
|
|
||||||
|
|
||||||
namespace Recyclarr.Common;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// An ASCII progress bar
|
|
||||||
/// </summary>
|
|
||||||
public sealed class ProgressBar //: IProgress<double>
|
|
||||||
{
|
|
||||||
private readonly IConsole _console;
|
|
||||||
private readonly TimeSpan _animationInterval = TimeSpan.FromSeconds(1.0 / 8);
|
|
||||||
private const string Animation = @"|/-\";
|
|
||||||
private int _animationIndex;
|
|
||||||
|
|
||||||
private readonly Subject<float> _reportProgress = new();
|
|
||||||
public IObserver<float> ReportProgress => _reportProgress;
|
|
||||||
public string Description { get; set; } = "";
|
|
||||||
|
|
||||||
public ProgressBar(IConsole console)
|
|
||||||
{
|
|
||||||
_console = console;
|
|
||||||
|
|
||||||
// A progress bar is only for temporary display in a console window.
|
|
||||||
// If the console output is redirected to a file, draw nothing.
|
|
||||||
// Otherwise, we'll end up with a lot of garbage in the target file.
|
|
||||||
if (!_console.IsOutputRedirected)
|
|
||||||
{
|
|
||||||
_reportProgress.Sample(_animationInterval)
|
|
||||||
.Select(CalculateText)
|
|
||||||
.StartWith(string.Empty)
|
|
||||||
.Buffer(2, 1) // sliding window: take previous and current
|
|
||||||
.Subscribe(x => UpdateText(x[0].Length, x[1]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string CalculateText(float progress)
|
|
||||||
{
|
|
||||||
const int blockCount = 10;
|
|
||||||
var progressBlockCount = (int) (progress * blockCount);
|
|
||||||
var percent = (int) (progress * 100);
|
|
||||||
var progressBlocks = new string('#', progressBlockCount);
|
|
||||||
var progressBlocksUnfilled = new string('-', blockCount - progressBlockCount);
|
|
||||||
var currentAnimationFrame = Animation[_animationIndex++ % Animation.Length];
|
|
||||||
return $"[{progressBlocks}{progressBlocksUnfilled}] {percent,3}% {currentAnimationFrame} {Description}";
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateText(int previousTextLength, string text)
|
|
||||||
{
|
|
||||||
var outputBuilder = new StringBuilder();
|
|
||||||
outputBuilder.Append('\r');
|
|
||||||
outputBuilder.Append(text);
|
|
||||||
|
|
||||||
// If the previous string was longer, "erase" the old characters with spaces.
|
|
||||||
var lengthDifference = previousTextLength - text.Length;
|
|
||||||
if (lengthDifference > 0)
|
|
||||||
{
|
|
||||||
outputBuilder.Append(' ', lengthDifference);
|
|
||||||
}
|
|
||||||
|
|
||||||
_console.Output.Write(outputBuilder);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,18 @@
|
|||||||
|
using Serilog.Core;
|
||||||
|
using Serilog.Events;
|
||||||
|
|
||||||
|
namespace Recyclarr.Common.Serilog;
|
||||||
|
|
||||||
|
public class ExceptionMessageEnricher : ILogEventEnricher
|
||||||
|
{
|
||||||
|
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
|
||||||
|
{
|
||||||
|
var msg = logEvent.Exception?.Message;
|
||||||
|
if (string.IsNullOrEmpty(msg))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("ExceptionMessage", msg));
|
||||||
|
}
|
||||||
|
}
|
@ -1,144 +0,0 @@
|
|||||||
using FluentValidation.TestHelper;
|
|
||||||
using NUnit.Framework;
|
|
||||||
using Recyclarr.TrashLib.Config.Services;
|
|
||||||
using Recyclarr.TrashLib.Services.Sonarr;
|
|
||||||
using Recyclarr.TrashLib.Services.Sonarr.Config;
|
|
||||||
|
|
||||||
namespace Recyclarr.TrashLib.Tests.Config;
|
|
||||||
|
|
||||||
[TestFixture]
|
|
||||||
[Parallelizable(ParallelScope.All)]
|
|
||||||
public class SonarrConfigurationValidatorTest
|
|
||||||
{
|
|
||||||
[Test]
|
|
||||||
public void Sonarr_v4_succeeds()
|
|
||||||
{
|
|
||||||
var config = new SonarrConfiguration
|
|
||||||
{
|
|
||||||
ApiKey = "valid",
|
|
||||||
BaseUrl = "valid",
|
|
||||||
CustomFormats = new List<CustomFormatConfig>
|
|
||||||
{
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
TrashIds = new List<string> {"valid"},
|
|
||||||
QualityProfiles = new List<QualityProfileScoreConfig>
|
|
||||||
{
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Name = "valid"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
QualityDefinition = new QualityDefinitionConfig
|
|
||||||
{
|
|
||||||
Type = "valid"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var capabilities = new SonarrCapabilities
|
|
||||||
{
|
|
||||||
SupportsCustomFormats = true,
|
|
||||||
SupportsNamedReleaseProfiles = true
|
|
||||||
};
|
|
||||||
var validator = new SonarrConfigurationValidator(capabilities);
|
|
||||||
var result = validator.TestValidate(config);
|
|
||||||
|
|
||||||
result.ShouldNotHaveAnyValidationErrors();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void Sonarr_v3_succeeds()
|
|
||||||
{
|
|
||||||
var config = new SonarrConfiguration
|
|
||||||
{
|
|
||||||
ApiKey = "valid",
|
|
||||||
BaseUrl = "valid",
|
|
||||||
ReleaseProfiles = new List<ReleaseProfileConfig>
|
|
||||||
{
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
TrashIds = new List<string> {"valid"},
|
|
||||||
Filter = new SonarrProfileFilterConfig {Include = new[] {"valid"}},
|
|
||||||
Tags = new[] {"valid"}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
QualityDefinition = new QualityDefinitionConfig
|
|
||||||
{
|
|
||||||
Type = "valid"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var capabilities = new SonarrCapabilities
|
|
||||||
{
|
|
||||||
SupportsCustomFormats = false,
|
|
||||||
SupportsNamedReleaseProfiles = true
|
|
||||||
};
|
|
||||||
var validator = new SonarrConfigurationValidator(capabilities);
|
|
||||||
var result = validator.TestValidate(config);
|
|
||||||
|
|
||||||
result.ShouldNotHaveAnyValidationErrors();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void Sonarr_v4_failures()
|
|
||||||
{
|
|
||||||
var config = new SonarrConfiguration
|
|
||||||
{
|
|
||||||
ReleaseProfiles = new List<ReleaseProfileConfig> {new()}
|
|
||||||
};
|
|
||||||
|
|
||||||
var capabilities = new SonarrCapabilities {SupportsCustomFormats = true};
|
|
||||||
var validator = new SonarrConfigurationValidator(capabilities);
|
|
||||||
var result = validator.TestValidate(config);
|
|
||||||
|
|
||||||
// Release profiles not allowed in v4
|
|
||||||
result.ShouldHaveValidationErrorFor(x => x.ReleaseProfiles);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void Sonarr_v3_failures()
|
|
||||||
{
|
|
||||||
var config = new SonarrConfiguration
|
|
||||||
{
|
|
||||||
CustomFormats = new List<CustomFormatConfig> {new()},
|
|
||||||
ReleaseProfiles = new List<ReleaseProfileConfig>
|
|
||||||
{
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
TrashIds = Array.Empty<string>(),
|
|
||||||
Filter = new SonarrProfileFilterConfig
|
|
||||||
{
|
|
||||||
Include = new[] {"include"},
|
|
||||||
Exclude = new[] {"exclude"}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var capabilities = new SonarrCapabilities
|
|
||||||
{
|
|
||||||
SupportsCustomFormats = false,
|
|
||||||
SupportsNamedReleaseProfiles = false
|
|
||||||
};
|
|
||||||
|
|
||||||
var validator = new SonarrConfigurationValidator(capabilities);
|
|
||||||
var result = validator.TestValidate(config);
|
|
||||||
|
|
||||||
// Custom formats not allowed in v3
|
|
||||||
result.ShouldHaveValidationErrorFor(x => x.CustomFormats);
|
|
||||||
|
|
||||||
// Due to named release profiles not being supported (minimum version requirement not met)
|
|
||||||
result.ShouldHaveValidationErrorFor(x => x);
|
|
||||||
|
|
||||||
var releaseProfiles = $"{nameof(config.ReleaseProfiles)}[0].";
|
|
||||||
|
|
||||||
// Release profile trash IDs cannot be empty
|
|
||||||
result.ShouldHaveValidationErrorFor(releaseProfiles + nameof(ReleaseProfileConfig.TrashIds));
|
|
||||||
|
|
||||||
// Cannot use include + exclude filters together
|
|
||||||
result.ShouldHaveValidationErrorFor(releaseProfiles +
|
|
||||||
$"{nameof(ReleaseProfileConfig.Filter)}.{nameof(SonarrProfileFilterConfig.Include)}");
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,58 @@
|
|||||||
|
using System.IO.Abstractions;
|
||||||
|
using System.IO.Abstractions.Extensions;
|
||||||
|
using FluentAssertions;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using Recyclarr.Cli.TestLibrary;
|
||||||
|
using Recyclarr.TestLibrary;
|
||||||
|
using Recyclarr.TrashLib.ExceptionTypes;
|
||||||
|
using Recyclarr.TrashLib.Services.Processors;
|
||||||
|
|
||||||
|
namespace Recyclarr.TrashLib.Tests.Services.Processors;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
[Parallelizable(ParallelScope.All)]
|
||||||
|
public class ConfigCreationProcessorTest : IntegrationFixture
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public async Task Config_file_created_when_using_default_path()
|
||||||
|
{
|
||||||
|
var sut = Resolve<ConfigCreationProcessor>();
|
||||||
|
|
||||||
|
await sut.Process(null);
|
||||||
|
|
||||||
|
var file = Fs.GetFile(Paths.ConfigPath);
|
||||||
|
file.Should().NotBeNull();
|
||||||
|
file.Contents.Should().NotBeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Config_file_created_when_using_user_specified_path()
|
||||||
|
{
|
||||||
|
var sut = Resolve<ConfigCreationProcessor>();
|
||||||
|
|
||||||
|
var ymlPath = Fs.CurrentDirectory()
|
||||||
|
.SubDirectory("user")
|
||||||
|
.SubDirectory("specified")
|
||||||
|
.File("file.yml");
|
||||||
|
|
||||||
|
await sut.Process(ymlPath.FullName);
|
||||||
|
|
||||||
|
var file = Fs.GetFile(ymlPath);
|
||||||
|
file.Should().NotBeNull();
|
||||||
|
file.Contents.Should().NotBeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Should_throw_if_file_already_exists()
|
||||||
|
{
|
||||||
|
var sut = Resolve<ConfigCreationProcessor>();
|
||||||
|
|
||||||
|
var yml = Fs.CurrentDirectory().File("file.yml");
|
||||||
|
Fs.AddEmptyFile(yml);
|
||||||
|
yml.Refresh(); // Required since file was created after IFileInfo was constructed
|
||||||
|
|
||||||
|
var act = () => sut.Process(yml.FullName);
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<FileExistsException>();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,95 @@
|
|||||||
|
using AutoFixture.NUnit3;
|
||||||
|
using FluentAssertions;
|
||||||
|
using NSubstitute;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using Recyclarr.TestLibrary.AutoFixture;
|
||||||
|
using Recyclarr.TrashLib.Config.Services;
|
||||||
|
using Recyclarr.TrashLib.ExceptionTypes;
|
||||||
|
using Recyclarr.TrashLib.Services.Sonarr.Capabilities;
|
||||||
|
using Recyclarr.TrashLib.Services.Sonarr.Config;
|
||||||
|
|
||||||
|
namespace Recyclarr.TrashLib.Tests.Sonarr;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
[Parallelizable(ParallelScope.All)]
|
||||||
|
public class SonarrCapabilityEnforcerTest
|
||||||
|
{
|
||||||
|
[Test, AutoMockData]
|
||||||
|
public void Fail_when_capabilities_not_obtained(
|
||||||
|
[Frozen] ISonarrCapabilityChecker checker,
|
||||||
|
SonarrCapabilityEnforcer sut)
|
||||||
|
{
|
||||||
|
var config = new SonarrConfiguration();
|
||||||
|
|
||||||
|
checker.GetCapabilities().Returns((SonarrCapabilities?) null);
|
||||||
|
|
||||||
|
var act = () => sut.Check(config);
|
||||||
|
|
||||||
|
act.Should().Throw<ServiceIncompatibilityException>().WithMessage("*obtained*");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test, AutoMockData]
|
||||||
|
public void Minimum_version_not_met(
|
||||||
|
[Frozen] ISonarrCapabilityChecker checker,
|
||||||
|
SonarrCapabilityEnforcer sut)
|
||||||
|
{
|
||||||
|
var config = new SonarrConfiguration();
|
||||||
|
|
||||||
|
checker.GetCapabilities().Returns(new SonarrCapabilities(new Version())
|
||||||
|
{
|
||||||
|
SupportsNamedReleaseProfiles = false
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = () => sut.Check(config);
|
||||||
|
|
||||||
|
act.Should().Throw<ServiceIncompatibilityException>().WithMessage("*minimum*");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test, AutoMockData]
|
||||||
|
public void Release_profiles_not_allowed_in_v4(
|
||||||
|
[Frozen] ISonarrCapabilityChecker checker,
|
||||||
|
SonarrCapabilityEnforcer sut)
|
||||||
|
{
|
||||||
|
var config = new SonarrConfiguration
|
||||||
|
{
|
||||||
|
ReleaseProfiles = new List<ReleaseProfileConfig>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checker.GetCapabilities().Returns(new SonarrCapabilities(new Version())
|
||||||
|
{
|
||||||
|
SupportsNamedReleaseProfiles = true,
|
||||||
|
SupportsCustomFormats = true
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = () => sut.Check(config);
|
||||||
|
|
||||||
|
act.Should().Throw<ServiceIncompatibilityException>().WithMessage("*v3*");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test, AutoMockData]
|
||||||
|
public void Custom_formats_not_allowed_in_v3(
|
||||||
|
[Frozen] ISonarrCapabilityChecker checker,
|
||||||
|
SonarrCapabilityEnforcer sut)
|
||||||
|
{
|
||||||
|
var config = new SonarrConfiguration
|
||||||
|
{
|
||||||
|
CustomFormats = new List<CustomFormatConfig>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checker.GetCapabilities().Returns(new SonarrCapabilities(new Version())
|
||||||
|
{
|
||||||
|
SupportsNamedReleaseProfiles = true,
|
||||||
|
SupportsCustomFormats = false
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = () => sut.Check(config);
|
||||||
|
|
||||||
|
act.Should().Throw<ServiceIncompatibilityException>().WithMessage("*v4*");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,70 @@
|
|||||||
|
using FluentValidation.TestHelper;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using Recyclarr.Cli.TestLibrary;
|
||||||
|
using Recyclarr.TrashLib.Config.Services;
|
||||||
|
using Recyclarr.TrashLib.Services.Sonarr.Config;
|
||||||
|
|
||||||
|
namespace Recyclarr.TrashLib.Tests.Sonarr;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
[Parallelizable(ParallelScope.All)]
|
||||||
|
public class SonarrConfigurationValidatorTest : IntegrationFixture
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void No_validation_failure_for_service_name()
|
||||||
|
{
|
||||||
|
var config = new SonarrConfiguration();
|
||||||
|
|
||||||
|
var validator = Resolve<SonarrConfigurationValidator>();
|
||||||
|
var result = validator.TestValidate(config);
|
||||||
|
|
||||||
|
result.ShouldNotHaveValidationErrorFor(x => x.ServiceName);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validation_failure_when_rps_and_cfs_used_together()
|
||||||
|
{
|
||||||
|
var config = new SonarrConfiguration
|
||||||
|
{
|
||||||
|
ReleaseProfiles = new[] {new ReleaseProfileConfig()},
|
||||||
|
CustomFormats = new[] {new CustomFormatConfig()}
|
||||||
|
};
|
||||||
|
|
||||||
|
var validator = Resolve<SonarrConfigurationValidator>();
|
||||||
|
var result = validator.TestValidate(config);
|
||||||
|
|
||||||
|
result.ShouldHaveValidationErrorFor(x => x.ReleaseProfiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Sonarr_release_profile_failures()
|
||||||
|
{
|
||||||
|
var config = new SonarrConfiguration
|
||||||
|
{
|
||||||
|
ReleaseProfiles = new List<ReleaseProfileConfig>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
TrashIds = Array.Empty<string>(),
|
||||||
|
Filter = new SonarrProfileFilterConfig
|
||||||
|
{
|
||||||
|
Include = new[] {"include"},
|
||||||
|
Exclude = new[] {"exclude"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var validator = new SonarrConfigurationValidator();
|
||||||
|
var result = validator.TestValidate(config);
|
||||||
|
|
||||||
|
var releaseProfiles = $"{nameof(config.ReleaseProfiles)}[0].";
|
||||||
|
|
||||||
|
// Release profile trash IDs cannot be empty
|
||||||
|
result.ShouldHaveValidationErrorFor(releaseProfiles + nameof(ReleaseProfileConfig.TrashIds));
|
||||||
|
|
||||||
|
// Cannot use include + exclude filters together
|
||||||
|
result.ShouldHaveValidationErrorFor(releaseProfiles +
|
||||||
|
$"{nameof(ReleaseProfileConfig.Filter)}.{nameof(SonarrProfileFilterConfig.Include)}");
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue