- Dedicated "Filter" system for YAML configuration that selectively excludes instances with issues such as duplicate, invalid, or split instances. - Greatly improved console output for configuration errors. - Recyclarr is now better about not completely exiting when there's an issue in a single configuration instance. Fixes #396pull/415/head
parent
ca8572b3a0
commit
cf075dabbf
@ -1,76 +0,0 @@
|
||||
using System.IO.Abstractions;
|
||||
using Recyclarr.Config.Parsing;
|
||||
using Spectre.Console;
|
||||
|
||||
namespace Recyclarr.Cli.Processors.Config;
|
||||
|
||||
/// <remarks>
|
||||
/// This was originally intended to be used by `config create`, but YamlDotNet cannot serialize
|
||||
/// comments so this
|
||||
/// class was not used. I kept it around in case I want to revisit later. There might be an
|
||||
/// opportunity to use this
|
||||
/// with the GUI.
|
||||
/// </remarks>
|
||||
public class ConfigManipulator(
|
||||
IAnsiConsole console,
|
||||
ConfigParser configParser,
|
||||
ConfigSaver configSaver,
|
||||
ConfigValidationExecutor validator
|
||||
) : IConfigManipulator
|
||||
{
|
||||
private static Dictionary<string, TConfig> InvokeCallbackForEach<TConfig>(
|
||||
Func<string, ServiceConfigYaml, ServiceConfigYaml> editCallback,
|
||||
IReadOnlyDictionary<string, TConfig>? configs
|
||||
)
|
||||
where TConfig : ServiceConfigYaml
|
||||
{
|
||||
var newConfigs = new Dictionary<string, TConfig>();
|
||||
|
||||
if (configs is null)
|
||||
{
|
||||
return newConfigs;
|
||||
}
|
||||
|
||||
foreach (var (instanceName, config) in configs)
|
||||
{
|
||||
newConfigs[instanceName] = (TConfig)editCallback(instanceName, config);
|
||||
}
|
||||
|
||||
return newConfigs;
|
||||
}
|
||||
|
||||
public void LoadAndSave(
|
||||
IFileInfo source,
|
||||
IFileInfo destinationFile,
|
||||
Func<string, ServiceConfigYaml, ServiceConfigYaml> editCallback
|
||||
)
|
||||
{
|
||||
// Parse & save the template file to address the following:
|
||||
// - Find & report any syntax errors
|
||||
// - Run validation & report issues
|
||||
// - Consistently reformat the output file (when it is saved again)
|
||||
// - Ignore stuff for diffing purposes, such as comments.
|
||||
var config = configParser.Load<RootConfigYaml>(source);
|
||||
if (config is null)
|
||||
{
|
||||
// Do not log here, since ConfigParser already has substantial logging
|
||||
throw new FileLoadException("Problem while loading config template");
|
||||
}
|
||||
|
||||
config = new RootConfigYaml
|
||||
{
|
||||
Radarr = InvokeCallbackForEach(editCallback, config.Radarr),
|
||||
Sonarr = InvokeCallbackForEach(editCallback, config.Sonarr),
|
||||
};
|
||||
|
||||
if (!validator.Validate(config, YamlValidatorRuleSets.RootConfig))
|
||||
{
|
||||
console.WriteLine(
|
||||
"The configuration file will still be created, despite the previous validation errors. "
|
||||
+ "You must open the file and correct the above issues before running a sync command."
|
||||
);
|
||||
}
|
||||
|
||||
configSaver.Save(config, destinationFile);
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
using System.IO.Abstractions;
|
||||
using Recyclarr.Config.Parsing;
|
||||
|
||||
namespace Recyclarr.Cli.Processors.Config;
|
||||
|
||||
public interface IConfigManipulator
|
||||
{
|
||||
void LoadAndSave(
|
||||
IFileInfo source,
|
||||
IFileInfo destinationFile,
|
||||
Func<string, ServiceConfigYaml, ServiceConfigYaml> editCallback
|
||||
);
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
using Recyclarr.TrashGuide;
|
||||
|
||||
namespace Recyclarr.Config;
|
||||
|
||||
public record ConfigFilterCriteria
|
||||
{
|
||||
public IReadOnlyCollection<string>? ManualConfigFiles { get; init; }
|
||||
public SupportedServices? Service { get; init; }
|
||||
public IReadOnlyCollection<string>? Instances { get; init; }
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
namespace Recyclarr.Config.ExceptionTypes;
|
||||
|
||||
public class DuplicateInstancesException(IReadOnlyCollection<string> instanceNames) : Exception
|
||||
public class DuplicateInstancesException(IReadOnlyCollection<string> instanceNames)
|
||||
: InvalidConfigurationException
|
||||
{
|
||||
public IReadOnlyCollection<string> InstanceNames { get; } = instanceNames;
|
||||
}
|
||||
|
@ -0,0 +1,3 @@
|
||||
namespace Recyclarr.Config.ExceptionTypes;
|
||||
|
||||
public class InvalidConfigurationException : Exception;
|
@ -1,6 +1,7 @@
|
||||
namespace Recyclarr.Config.ExceptionTypes;
|
||||
|
||||
public class InvalidInstancesException(IReadOnlyCollection<string> instanceNames) : Exception
|
||||
public class InvalidInstancesException(IReadOnlyCollection<string> instanceNames)
|
||||
: InvalidConfigurationException
|
||||
{
|
||||
public IReadOnlyCollection<string> InstanceNames { get; } = instanceNames;
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
namespace Recyclarr.Config.ExceptionTypes;
|
||||
|
||||
public class SplitInstancesException(IReadOnlyCollection<string> instanceNames) : Exception
|
||||
public class SplitInstancesException(IReadOnlyCollection<string> instanceNames)
|
||||
: InvalidConfigurationException
|
||||
{
|
||||
public IReadOnlyCollection<string> InstanceNames { get; } = instanceNames;
|
||||
}
|
||||
|
@ -0,0 +1,20 @@
|
||||
using Recyclarr.Config.Parsing;
|
||||
using Recyclarr.TrashGuide;
|
||||
|
||||
namespace Recyclarr.Config.Filtering;
|
||||
|
||||
public record ConfigFilterCriteria
|
||||
{
|
||||
public IReadOnlyCollection<string> ManualConfigFiles { get; init; } = [];
|
||||
public SupportedServices? Service { get; init; }
|
||||
public IReadOnlyCollection<string> Instances { get; init; } = [];
|
||||
|
||||
public bool InstanceMatchesCriteria(LoadedConfigYaml loadedConfig)
|
||||
{
|
||||
return (Service is null || Service == loadedConfig.ServiceType)
|
||||
&& (
|
||||
Instances.Count == 0
|
||||
|| Instances.Contains(loadedConfig.InstanceName, StringComparer.OrdinalIgnoreCase)
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
using Recyclarr.Config.Parsing;
|
||||
using Spectre.Console;
|
||||
|
||||
namespace Recyclarr.Config.Filtering;
|
||||
|
||||
public class ConfigFilterProcessor(IAnsiConsole console, IEnumerable<IConfigFilter> filters)
|
||||
{
|
||||
public IReadOnlyCollection<LoadedConfigYaml> FilterAndRender(
|
||||
ConfigFilterCriteria criteria,
|
||||
IReadOnlyCollection<LoadedConfigYaml> configs
|
||||
)
|
||||
{
|
||||
var context = new FilterContext();
|
||||
|
||||
var filteredConfigs = filters.Aggregate(
|
||||
configs,
|
||||
(current, filter) => filter.Filter(criteria, current, context)
|
||||
);
|
||||
|
||||
var renderables = context
|
||||
.Results.Select(x => new Padder(x.Render()).Padding(0, 0, 0, 1))
|
||||
.ToList();
|
||||
|
||||
if (renderables.Count != 0)
|
||||
{
|
||||
var main = new Panel(new Padder(new Rows(renderables).Collapse()).PadBottom(0))
|
||||
.Collapse()
|
||||
.Header("[red]Configuration Errors[/]")
|
||||
.RoundedBorder();
|
||||
|
||||
var column = new Columns(main);
|
||||
|
||||
console.Write(column);
|
||||
}
|
||||
|
||||
return filteredConfigs;
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
using Recyclarr.Config.Parsing;
|
||||
using Spectre.Console;
|
||||
using Spectre.Console.Rendering;
|
||||
|
||||
namespace Recyclarr.Config.Filtering;
|
||||
|
||||
public class DuplicateInstancesFilter(ILogger log) : IConfigFilter
|
||||
{
|
||||
public IReadOnlyCollection<LoadedConfigYaml> Filter(
|
||||
ConfigFilterCriteria criteria,
|
||||
IReadOnlyCollection<LoadedConfigYaml> configs,
|
||||
FilterContext context
|
||||
)
|
||||
{
|
||||
var duplicateInstances = configs
|
||||
.Select(x => x.InstanceName)
|
||||
.GroupBy(x => x, StringComparer.InvariantCultureIgnoreCase)
|
||||
.Where(x => x.Count() > 1)
|
||||
.Select(x => x.First())
|
||||
.ToList();
|
||||
|
||||
if (duplicateInstances.Count != 0)
|
||||
{
|
||||
context.AddResult(new DuplicateInstancesFilterResult(duplicateInstances));
|
||||
log.Debug("Duplicate instances: {Instances}", duplicateInstances);
|
||||
}
|
||||
|
||||
return configs
|
||||
.ExceptBy(
|
||||
duplicateInstances,
|
||||
x => x.InstanceName,
|
||||
StringComparer.InvariantCultureIgnoreCase
|
||||
)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public class DuplicateInstancesFilterResult(IReadOnlyCollection<string> duplicateInstances)
|
||||
: IFilterResult
|
||||
{
|
||||
public IReadOnlyCollection<string> DuplicateInstances => duplicateInstances;
|
||||
|
||||
public IRenderable Render()
|
||||
{
|
||||
var tree = new Tree("[orange1]Duplicate Instances[/]");
|
||||
tree.AddNodes(duplicateInstances);
|
||||
return tree;
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
namespace Recyclarr.Config.Filtering;
|
||||
|
||||
public class FilterContext
|
||||
{
|
||||
private readonly List<IFilterResult> _results = [];
|
||||
|
||||
public IReadOnlyCollection<IFilterResult> Results => _results;
|
||||
|
||||
public void AddResult(IFilterResult result) => _results.Add(result);
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
using Recyclarr.Config.Parsing;
|
||||
|
||||
namespace Recyclarr.Config.Filtering;
|
||||
|
||||
public interface IConfigFilter
|
||||
{
|
||||
IReadOnlyCollection<LoadedConfigYaml> Filter(
|
||||
ConfigFilterCriteria criteria,
|
||||
IReadOnlyCollection<LoadedConfigYaml> configs,
|
||||
FilterContext context
|
||||
);
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
using Spectre.Console.Rendering;
|
||||
|
||||
namespace Recyclarr.Config.Filtering;
|
||||
|
||||
public interface IFilterResult
|
||||
{
|
||||
IRenderable Render();
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
using Recyclarr.Config.Parsing;
|
||||
using Spectre.Console;
|
||||
using Spectre.Console.Rendering;
|
||||
|
||||
namespace Recyclarr.Config.Filtering;
|
||||
|
||||
public record ConfigValidationErrorInfo(
|
||||
string InstanceName,
|
||||
IReadOnlyCollection<ValidationFailure> Failures
|
||||
);
|
||||
|
||||
public class InvalidInstancesFilter(ILogger log, IValidator<ServiceConfigYaml> validator)
|
||||
: IConfigFilter
|
||||
{
|
||||
public IReadOnlyCollection<LoadedConfigYaml> Filter(
|
||||
ConfigFilterCriteria criteria,
|
||||
IReadOnlyCollection<LoadedConfigYaml> configs,
|
||||
FilterContext context
|
||||
)
|
||||
{
|
||||
var invalid = configs
|
||||
.Select(config =>
|
||||
(
|
||||
config.InstanceName,
|
||||
Result: validator.Validate(
|
||||
config.Yaml,
|
||||
options =>
|
||||
options
|
||||
.IncludeRulesNotInRuleSet()
|
||||
.IncludeRuleSets(YamlValidatorRuleSets.RootConfig)
|
||||
)
|
||||
)
|
||||
)
|
||||
.Where(x => !x.Result.IsValid)
|
||||
.Select(r => new ConfigValidationErrorInfo(r.InstanceName, r.Result.Errors))
|
||||
.ToList();
|
||||
|
||||
if (invalid.Count != 0)
|
||||
{
|
||||
context.AddResult(new InvalidInstancesFilterResult(invalid));
|
||||
log.Debug(
|
||||
"Invalid instances: {@Instances}",
|
||||
invalid.Select(x => new
|
||||
{
|
||||
x.InstanceName,
|
||||
Errors = x.Failures.Select(f => f.ErrorMessage),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return configs
|
||||
.ExceptBy(
|
||||
invalid.Select(x => x.InstanceName),
|
||||
x => x.InstanceName,
|
||||
StringComparer.InvariantCultureIgnoreCase
|
||||
)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public class InvalidInstancesFilterResult(
|
||||
IReadOnlyCollection<ConfigValidationErrorInfo> invalidInstances
|
||||
) : IFilterResult
|
||||
{
|
||||
public IReadOnlyCollection<ConfigValidationErrorInfo> InvalidInstances => invalidInstances;
|
||||
|
||||
public IRenderable Render()
|
||||
{
|
||||
var tree = new Tree("[orange1]Invalid Instances[/]");
|
||||
|
||||
foreach (var (instanceName, failures) in invalidInstances)
|
||||
{
|
||||
var instanceNode = tree.AddNode($"[cornflowerblue]{instanceName}[/]");
|
||||
|
||||
foreach (var f in failures)
|
||||
{
|
||||
var prefix = GetSeverityPrefix(f.Severity);
|
||||
instanceNode.AddNode($"{prefix} {f.ErrorMessage}");
|
||||
}
|
||||
}
|
||||
|
||||
return tree;
|
||||
}
|
||||
|
||||
private static string GetSeverityPrefix(Severity severity)
|
||||
{
|
||||
return severity switch
|
||||
{
|
||||
Severity.Error => "[red]X[/]",
|
||||
Severity.Warning => "[yellow]![/]",
|
||||
Severity.Info => "[blue]i[/]",
|
||||
_ => "[grey]?[/]",
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
using Recyclarr.Config.Parsing;
|
||||
using Spectre.Console;
|
||||
using Spectre.Console.Rendering;
|
||||
|
||||
namespace Recyclarr.Config.Filtering;
|
||||
|
||||
public class NonExistentInstancesFilter(ILogger log) : IConfigFilter
|
||||
{
|
||||
public IReadOnlyCollection<LoadedConfigYaml> Filter(
|
||||
ConfigFilterCriteria criteria,
|
||||
IReadOnlyCollection<LoadedConfigYaml> configs,
|
||||
FilterContext context
|
||||
)
|
||||
{
|
||||
if (criteria.Instances is { Count: > 0 })
|
||||
{
|
||||
var names = configs.Select(x => x.InstanceName).ToList();
|
||||
|
||||
var nonExistentInstances = criteria
|
||||
.Instances.Where(x => !names.Contains(x, StringComparer.InvariantCultureIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
context.AddResult(new NonExistentInstancesFilterResult(nonExistentInstances));
|
||||
log.Debug("Non-existent instances: {Instances}", nonExistentInstances);
|
||||
}
|
||||
|
||||
return configs;
|
||||
}
|
||||
}
|
||||
|
||||
public class NonExistentInstancesFilterResult(IReadOnlyCollection<string> nonExistentInstances)
|
||||
: IFilterResult
|
||||
{
|
||||
public IReadOnlyCollection<string> NonExistentInstances => nonExistentInstances;
|
||||
|
||||
public IRenderable Render()
|
||||
{
|
||||
var tree = new Tree("[orange1]Non-Existent Instances[/]");
|
||||
tree.AddNodes(nonExistentInstances);
|
||||
return tree;
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Recyclarr.Config.Parsing;
|
||||
using Spectre.Console;
|
||||
using Spectre.Console.Rendering;
|
||||
|
||||
namespace Recyclarr.Config.Filtering;
|
||||
|
||||
[SuppressMessage("Design", "CA1054:URI-like parameters should not be strings")]
|
||||
[SuppressMessage("Design", "CA1056:URI-like properties should not be strings")]
|
||||
public record SplitInstanceErrorInfo(string BaseUrl, IReadOnlyCollection<string> InstanceNames);
|
||||
|
||||
public class SplitInstancesFilter(ILogger log) : IConfigFilter
|
||||
{
|
||||
public IReadOnlyCollection<LoadedConfigYaml> Filter(
|
||||
ConfigFilterCriteria criteria,
|
||||
IReadOnlyCollection<LoadedConfigYaml> configs,
|
||||
FilterContext context
|
||||
)
|
||||
{
|
||||
var splitInstances = configs
|
||||
.Where(x => x.Yaml.BaseUrl is not null)
|
||||
.GroupBy(x => x.Yaml.BaseUrl!)
|
||||
.Where(x => x.Count() > 1)
|
||||
.Select(x => new SplitInstanceErrorInfo(x.Key, x.Select(y => y.InstanceName).ToList()))
|
||||
.ToList();
|
||||
|
||||
if (splitInstances.Count != 0)
|
||||
{
|
||||
context.AddResult(new SplitInstancesFilterResult(splitInstances));
|
||||
log.Debug(
|
||||
"Split instances: {@Instances}",
|
||||
// Anonymous object to avoid "$type" property in logs
|
||||
splitInstances.Select(x => new { x.BaseUrl, x.InstanceNames })
|
||||
);
|
||||
}
|
||||
|
||||
return configs
|
||||
.ExceptBy(
|
||||
splitInstances.SelectMany(x => x.InstanceNames),
|
||||
x => x.InstanceName,
|
||||
StringComparer.InvariantCultureIgnoreCase
|
||||
)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public class SplitInstancesFilterResult(IReadOnlyCollection<SplitInstanceErrorInfo> splitInstances)
|
||||
: IFilterResult
|
||||
{
|
||||
public IReadOnlyCollection<SplitInstanceErrorInfo> SplitInstances => splitInstances;
|
||||
|
||||
public IRenderable Render()
|
||||
{
|
||||
var tree = new Tree("[orange1]Split Instances[/]");
|
||||
|
||||
foreach (var (baseUrl, instanceNames) in splitInstances)
|
||||
{
|
||||
var instanceTree = new Tree($"[cornflowerblue]Base URL:[/] {baseUrl}");
|
||||
instanceTree.AddNodes(instanceNames);
|
||||
tree.AddNode(instanceTree);
|
||||
}
|
||||
|
||||
return tree;
|
||||
}
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
using Recyclarr.Config.Models;
|
||||
|
||||
namespace Recyclarr.Config;
|
||||
|
||||
public interface IConfigurationRegistry
|
||||
{
|
||||
IReadOnlyCollection<IServiceConfiguration> FindAndLoadConfigs(
|
||||
ConfigFilterCriteria? filterCriteria = null
|
||||
);
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
using System.IO.Abstractions;
|
||||
using Recyclarr.Config.Models;
|
||||
|
||||
namespace Recyclarr.Config.Parsing;
|
||||
|
||||
public interface IConfigurationLoader
|
||||
{
|
||||
IReadOnlyCollection<IServiceConfiguration> Load(IFileInfo file);
|
||||
IReadOnlyCollection<IServiceConfiguration> Load(string yaml);
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
using System.IO.Abstractions;
|
||||
using Recyclarr.Cli.Processors.Config;
|
||||
|
||||
namespace Recyclarr.Cli.IntegrationTests;
|
||||
|
||||
[TestFixture]
|
||||
internal class ConfigManipulatorTest : CliIntegrationFixture
|
||||
{
|
||||
[Test]
|
||||
public void Create_file_when_no_file_already_exists()
|
||||
{
|
||||
var sut = Resolve<ConfigManipulator>();
|
||||
var src = Fs.CurrentDirectory().File("template.yml");
|
||||
var dst = Fs.CurrentDirectory().SubDirectory("one", "two", "three").File("config.yml");
|
||||
|
||||
const string yamlData = """
|
||||
sonarr:
|
||||
instance1:
|
||||
base_url: http://localhost:80
|
||||
api_key: 123abc
|
||||
""";
|
||||
|
||||
Fs.AddFile(src, new MockFileData(yamlData));
|
||||
|
||||
sut.LoadAndSave(src, dst, (_, yaml) => yaml);
|
||||
|
||||
Fs.AllFiles.Should().Contain(dst.FullName);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Throw_on_invalid_yaml()
|
||||
{
|
||||
var sut = Resolve<ConfigManipulator>();
|
||||
var src = Fs.CurrentDirectory().File("template.yml");
|
||||
var dst = Fs.CurrentDirectory().File("config.yml");
|
||||
|
||||
const string yamlData = """
|
||||
sonarr:
|
||||
instance1:
|
||||
invalid: yaml
|
||||
""";
|
||||
|
||||
Fs.AddFile(src, new MockFileData(yamlData));
|
||||
|
||||
var act = () => sut.LoadAndSave(src, dst, (_, yaml) => yaml);
|
||||
|
||||
act.Should().Throw<FileLoadException>();
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
using Recyclarr.Cli.Console.Commands;
|
||||
using Recyclarr.Repo;
|
||||
|
||||
namespace Recyclarr.Cli.Tests.Console.Commands;
|
||||
|
||||
[TestFixture]
|
||||
public class ConfigCommandsTest
|
||||
{
|
||||
[Test, AutoMockData]
|
||||
public async Task Repo_update_is_called_on_config_list(
|
||||
[Frozen] IMultiRepoUpdater updater,
|
||||
ConfigListLocalCommand sut
|
||||
)
|
||||
{
|
||||
await sut.ExecuteAsync(default!, new ConfigListLocalCommand.CliSettings());
|
||||
|
||||
await updater.ReceivedWithAnyArgs().UpdateAllRepositories(default);
|
||||
}
|
||||
|
||||
[Test, AutoMockData]
|
||||
public async Task Repo_update_is_called_on_config_create(
|
||||
[Frozen] IMultiRepoUpdater updater,
|
||||
ConfigCreateCommand sut
|
||||
)
|
||||
{
|
||||
await sut.ExecuteAsync(default!, new ConfigCreateCommand.CliSettings());
|
||||
|
||||
await updater.ReceivedWithAnyArgs().UpdateAllRepositories(default);
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
using Recyclarr.Cli.Console.Commands;
|
||||
using Recyclarr.Repo;
|
||||
|
||||
namespace Recyclarr.Cli.Tests.Console.Commands;
|
||||
|
||||
[TestFixture]
|
||||
public class ListCommandsTest
|
||||
{
|
||||
[Test, AutoMockData]
|
||||
public async Task Repo_update_is_called_on_list_custom_formats(
|
||||
[Frozen] IMultiRepoUpdater updater,
|
||||
ListCustomFormatsCommand sut
|
||||
)
|
||||
{
|
||||
await sut.ExecuteAsync(default!, new ListCustomFormatsCommand.CliSettings());
|
||||
|
||||
await updater.ReceivedWithAnyArgs().UpdateAllRepositories(default);
|
||||
}
|
||||
|
||||
[Test, AutoMockData]
|
||||
public async Task Repo_update_is_called_on_list_qualities(
|
||||
[Frozen] IMultiRepoUpdater updater,
|
||||
ListQualitiesCommand sut
|
||||
)
|
||||
{
|
||||
await sut.ExecuteAsync(default!, new ListQualitiesCommand.CliSettings());
|
||||
|
||||
await updater.ReceivedWithAnyArgs().UpdateAllRepositories(default);
|
||||
}
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
{
|
||||
"name": "Optionals",
|
||||
"trash_id": "76e060895c5b8a765c310933da0a5357",
|
||||
"ignored": [{
|
||||
"name": "Golden rule",
|
||||
"trash_id": "cec8880b847dd5d31d29167ee0112b57",
|
||||
"term": "/^(?=.*(1080|720))(?=.*((x|h)[ ._-]?265|hevc)).*/i"
|
||||
}, {
|
||||
"name": "Ignore Dolby Vision without HDR10 fallback.",
|
||||
"trash_id": "436f5a7d08fbf02ba25cb5e5dfe98e55",
|
||||
"term": "/^(?!.*(HDR|HULU|REMUX))(?=.*\\b(DV|Dovi|Dolby[- .]?Vision)\\b).*/i"
|
||||
}, {
|
||||
"name": "Ignore The Group -SCENE",
|
||||
"trash_id": "f3f0f3691c6a1988d4a02963e69d11f2",
|
||||
"term": "/\\b(-scene)\\b/i"
|
||||
}, {
|
||||
"name": "Ignore so called scene releases",
|
||||
"trash_id": "5bc23c3a055a1a5d8bbe4fb49d80e0cb",
|
||||
"term": "/^(?!.*(web[ ]dl|-deflate|-inflate))(?=.*([_. ]WEB[_. ]|-CAKES\\b|-GGEZ\\b|-GGWP\\b|-GLHF\\b|-GOSSIP\\b|-KOGI\\b|-PECULATE\\b)).*/i"
|
||||
}, {
|
||||
"name": "Dislike Bad Dual Audio Groups",
|
||||
"trash_id": "538bad00ee6f8aced8e0db5218b8484c",
|
||||
"term": "/\\b(-alfaHD|-BAT|-BNd|-C\\.A\\.A|-Cory|-FF|-FOXX|-G4RiS|-GUEIRA|-N3G4N|-PD|-RiPER|-RK|-SiGLA|-Tars|-WTV|-Yatogam1|-YusukeFLA)\\b/i"
|
||||
}],
|
||||
"required": [],
|
||||
"preferred": [{
|
||||
"score": 15,
|
||||
"terms": [{
|
||||
"name": "Prefer Season Packs",
|
||||
"trash_id": "ea83f4740cec4df8112f3d6dd7c82751",
|
||||
"term": "/\\bS\\d+\\b(?!E\\d+\\b)/i"
|
||||
}]
|
||||
}, {
|
||||
"score": 10,
|
||||
"terms": [{
|
||||
"name": "Prefer HDR",
|
||||
"trash_id": "bc7a6383cbe88c3ee2d6396e1aacc0b3",
|
||||
"term": "/\\bHDR(\\b|\\d)/i"
|
||||
}]
|
||||
}, {
|
||||
"score": 100,
|
||||
"terms": [{
|
||||
"name": "Prefer Dolby Vision",
|
||||
"trash_id": "fa47da3377076d82d07c4e95b3f13d07",
|
||||
"term": "/\\b(dv|dovi|dolby[ .]?vision)\\b/i"
|
||||
}]
|
||||
}, {
|
||||
"score": -25,
|
||||
"terms": [{
|
||||
"name": "Dislike retags: rartv, rarbg, eztv, TGx",
|
||||
"trash_id": "6f2aefa61342a63387f2a90489e90790",
|
||||
"term": "/(\\[rartv\\]|\\[rarbg\\]|\\[eztv\\]|\\[TGx\\])/i"
|
||||
}, {
|
||||
"name": "Dislike retagged groups",
|
||||
"trash_id": "19cd5ecc0a24bf493a75e80a51974cdd",
|
||||
"term": "/(-4P|-4Planet|-AsRequested|-BUYMORE|-CAPTCHA|-Chamele0n|-GEROV|-iNC0GNiTO|-NZBGeek|-Obfuscated|-postbot|-Rakuv|-Scrambled|-WhiteRev|-WRTEAM|-xpost)\\b/i"
|
||||
}, {
|
||||
"name": "Dislike release ending: en",
|
||||
"trash_id": "6a7b462c6caee4a991a9d8aa38ce2405",
|
||||
"term": "/\\s?\\ben\\b$/i"
|
||||
}, {
|
||||
"name": "Dislike release containing: 1-",
|
||||
"trash_id": "236a3626a07cacf5692c73cc947bc280",
|
||||
"term": "/(?<!\\d\\.)(1-.+)$/i"
|
||||
}]
|
||||
}]
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
sonarr:
|
||||
name:
|
||||
base_url: http://localhost:8989
|
||||
api_key: 95283e6b156c42f3af8a9b16173f876b
|
@ -1,157 +1,138 @@
|
||||
using Recyclarr.Config;
|
||||
using Recyclarr.Config.Models;
|
||||
using Recyclarr.TrashGuide;
|
||||
|
||||
namespace Recyclarr.Tests.Config;
|
||||
|
||||
[TestFixture]
|
||||
public class ConfigExtensionsTest
|
||||
{
|
||||
[Test]
|
||||
public void Filter_invalid_instances()
|
||||
{
|
||||
var configs = new[]
|
||||
{
|
||||
new RadarrConfiguration
|
||||
{
|
||||
InstanceName = "valid_NAME", // Comparison should be case-insensitive
|
||||
},
|
||||
};
|
||||
|
||||
var invalidInstanceNames = configs.GetInvalidInstanceNames(
|
||||
new ConfigFilterCriteria { Instances = ["valid_name", "invalid_name"] }
|
||||
);
|
||||
|
||||
invalidInstanceNames.Should().BeEquivalentTo("invalid_name");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Filter_invalid_instances_when_null()
|
||||
{
|
||||
var configs = new[]
|
||||
{
|
||||
new RadarrConfiguration
|
||||
{
|
||||
InstanceName = "valid_NAME", // Comparison should be case-insensitive
|
||||
},
|
||||
};
|
||||
|
||||
var invalidInstanceNames = configs.GetInvalidInstanceNames(
|
||||
new ConfigFilterCriteria { Instances = null }
|
||||
);
|
||||
|
||||
invalidInstanceNames.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Get_configs_matching_service_type_and_instance_name()
|
||||
{
|
||||
var configs = new IServiceConfiguration[]
|
||||
{
|
||||
new RadarrConfiguration { InstanceName = "radarr1" },
|
||||
new RadarrConfiguration { InstanceName = "radarr2" },
|
||||
new RadarrConfiguration { InstanceName = "radarr3" },
|
||||
new RadarrConfiguration { InstanceName = "radarr4" },
|
||||
new SonarrConfiguration { InstanceName = "sonarr1" },
|
||||
new SonarrConfiguration { InstanceName = "sonarr2" },
|
||||
new SonarrConfiguration { InstanceName = "sonarr3" },
|
||||
new SonarrConfiguration { InstanceName = "sonarr4" },
|
||||
};
|
||||
|
||||
var result = configs.GetConfigsBasedOnSettings(
|
||||
new ConfigFilterCriteria
|
||||
{
|
||||
Service = SupportedServices.Radarr,
|
||||
Instances = ["radarr2", "radarr4", "radarr5", "sonarr2"],
|
||||
}
|
||||
);
|
||||
|
||||
result.Select(x => x.InstanceName).Should().BeEquivalentTo("radarr2", "radarr4");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Get_configs_based_on_settings_with_empty_instances()
|
||||
{
|
||||
var configs = new IServiceConfiguration[]
|
||||
{
|
||||
new RadarrConfiguration { InstanceName = "radarr1" },
|
||||
new SonarrConfiguration { InstanceName = "sonarr1" },
|
||||
};
|
||||
|
||||
var result = configs.GetConfigsBasedOnSettings(
|
||||
new ConfigFilterCriteria { Instances = Array.Empty<string>() }
|
||||
);
|
||||
|
||||
result.Select(x => x.InstanceName).Should().BeEquivalentTo("radarr1", "sonarr1");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Get_split_instance_names()
|
||||
{
|
||||
var configs = new IServiceConfiguration[]
|
||||
{
|
||||
new RadarrConfiguration
|
||||
{
|
||||
InstanceName = "radarr1",
|
||||
BaseUrl = new Uri("http://radarr1"),
|
||||
},
|
||||
new RadarrConfiguration
|
||||
{
|
||||
InstanceName = "radarr2",
|
||||
BaseUrl = new Uri("http://radarr1"),
|
||||
},
|
||||
new RadarrConfiguration
|
||||
{
|
||||
InstanceName = "radarr3",
|
||||
BaseUrl = new Uri("http://radarr3"),
|
||||
},
|
||||
new RadarrConfiguration
|
||||
{
|
||||
InstanceName = "radarr4",
|
||||
BaseUrl = new Uri("http://radarr4"),
|
||||
},
|
||||
new SonarrConfiguration
|
||||
{
|
||||
InstanceName = "sonarr1",
|
||||
BaseUrl = new Uri("http://sonarr1"),
|
||||
},
|
||||
new SonarrConfiguration
|
||||
{
|
||||
InstanceName = "sonarr2",
|
||||
BaseUrl = new Uri("http://sonarr2"),
|
||||
},
|
||||
new SonarrConfiguration
|
||||
{
|
||||
InstanceName = "sonarr3",
|
||||
BaseUrl = new Uri("http://sonarr2"),
|
||||
},
|
||||
new SonarrConfiguration
|
||||
{
|
||||
InstanceName = "sonarr4",
|
||||
BaseUrl = new Uri("http://sonarr4"),
|
||||
},
|
||||
};
|
||||
|
||||
var result = configs.GetSplitInstances();
|
||||
|
||||
result.Should().BeEquivalentTo("radarr1", "radarr2", "sonarr2", "sonarr3");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Get_duplicate_instance_names()
|
||||
{
|
||||
var configs = new IServiceConfiguration[]
|
||||
{
|
||||
new RadarrConfiguration { InstanceName = "radarr1" },
|
||||
new RadarrConfiguration { InstanceName = "radarr2" },
|
||||
new RadarrConfiguration { InstanceName = "radarr2" },
|
||||
new RadarrConfiguration { InstanceName = "radarr3" },
|
||||
new SonarrConfiguration { InstanceName = "sonarr1" },
|
||||
new SonarrConfiguration { InstanceName = "sonarr1" },
|
||||
};
|
||||
|
||||
var result = configs.GetDuplicateInstanceNames();
|
||||
|
||||
result.Should().BeEquivalentTo("radarr2", "sonarr1");
|
||||
}
|
||||
}
|
||||
// using Recyclarr.Config;
|
||||
// using Recyclarr.Config.Models;
|
||||
// using Recyclarr.Config.Parsing;
|
||||
// using Recyclarr.TrashGuide;
|
||||
//
|
||||
// namespace Recyclarr.Tests.Config;
|
||||
//
|
||||
// [TestFixture]
|
||||
// public class ConfigExtensionsTest
|
||||
// {
|
||||
// [Test]
|
||||
// public void Filter_invalid_instances()
|
||||
// {
|
||||
// var configs = new[]
|
||||
// {
|
||||
// new LoadedConfigYaml("valid_NAME", SupportedServices.Sonarr, new ServiceConfigYaml()),
|
||||
// };
|
||||
//
|
||||
// // Comparison should be case-insensitive
|
||||
// var invalidInstanceNames = configs.GetNonExistentInstanceNames(
|
||||
// new ConfigFilterCriteria { Instances = ["valid_name", "invalid_name"] }
|
||||
// );
|
||||
//
|
||||
// invalidInstanceNames.Should().BeEquivalentTo("invalid_name");
|
||||
// }
|
||||
//
|
||||
// [Test]
|
||||
// public void Get_configs_matching_service_type_and_instance_name()
|
||||
// {
|
||||
// var configs = new IServiceConfiguration[]
|
||||
// {
|
||||
// new RadarrConfiguration { InstanceName = "radarr1" },
|
||||
// new RadarrConfiguration { InstanceName = "radarr2" },
|
||||
// new RadarrConfiguration { InstanceName = "radarr3" },
|
||||
// new RadarrConfiguration { InstanceName = "radarr4" },
|
||||
// new SonarrConfiguration { InstanceName = "sonarr1" },
|
||||
// new SonarrConfiguration { InstanceName = "sonarr2" },
|
||||
// new SonarrConfiguration { InstanceName = "sonarr3" },
|
||||
// new SonarrConfiguration { InstanceName = "sonarr4" },
|
||||
// };
|
||||
//
|
||||
// var result = configs.GetConfigsBasedOnSettings(
|
||||
// new ConfigFilterCriteria
|
||||
// {
|
||||
// Service = SupportedServices.Radarr,
|
||||
// Instances = ["radarr2", "radarr4", "radarr5", "sonarr2"],
|
||||
// }
|
||||
// );
|
||||
//
|
||||
// result.Select(x => x.InstanceName).Should().BeEquivalentTo("radarr2", "radarr4");
|
||||
// }
|
||||
//
|
||||
// [Test]
|
||||
// public void Get_configs_based_on_settings_with_empty_instances()
|
||||
// {
|
||||
// var configs = new IServiceConfiguration[]
|
||||
// {
|
||||
// new RadarrConfiguration { InstanceName = "radarr1" },
|
||||
// new SonarrConfiguration { InstanceName = "sonarr1" },
|
||||
// };
|
||||
//
|
||||
// var result = configs.GetConfigsBasedOnSettings(
|
||||
// new ConfigFilterCriteria { Instances = Array.Empty<string>() }
|
||||
// );
|
||||
//
|
||||
// result.Select(x => x.InstanceName).Should().BeEquivalentTo("radarr1", "sonarr1");
|
||||
// }
|
||||
//
|
||||
// [Test]
|
||||
// public void Get_split_instance_names()
|
||||
// {
|
||||
// var configs = new IServiceConfiguration[]
|
||||
// {
|
||||
// new RadarrConfiguration
|
||||
// {
|
||||
// InstanceName = "radarr1",
|
||||
// BaseUrl = new Uri("http://radarr1"),
|
||||
// },
|
||||
// new RadarrConfiguration
|
||||
// {
|
||||
// InstanceName = "radarr2",
|
||||
// BaseUrl = new Uri("http://radarr1"),
|
||||
// },
|
||||
// new RadarrConfiguration
|
||||
// {
|
||||
// InstanceName = "radarr3",
|
||||
// BaseUrl = new Uri("http://radarr3"),
|
||||
// },
|
||||
// new RadarrConfiguration
|
||||
// {
|
||||
// InstanceName = "radarr4",
|
||||
// BaseUrl = new Uri("http://radarr4"),
|
||||
// },
|
||||
// new SonarrConfiguration
|
||||
// {
|
||||
// InstanceName = "sonarr1",
|
||||
// BaseUrl = new Uri("http://sonarr1"),
|
||||
// },
|
||||
// new SonarrConfiguration
|
||||
// {
|
||||
// InstanceName = "sonarr2",
|
||||
// BaseUrl = new Uri("http://sonarr2"),
|
||||
// },
|
||||
// new SonarrConfiguration
|
||||
// {
|
||||
// InstanceName = "sonarr3",
|
||||
// BaseUrl = new Uri("http://sonarr2"),
|
||||
// },
|
||||
// new SonarrConfiguration
|
||||
// {
|
||||
// InstanceName = "sonarr4",
|
||||
// BaseUrl = new Uri("http://sonarr4"),
|
||||
// },
|
||||
// };
|
||||
//
|
||||
// var result = configs.GetSplitInstances();
|
||||
//
|
||||
// result.Should().BeEquivalentTo("radarr1", "radarr2", "sonarr2", "sonarr3");
|
||||
// }
|
||||
//
|
||||
// [Test]
|
||||
// public void Get_duplicate_instance_names()
|
||||
// {
|
||||
// var configs = new IServiceConfiguration[]
|
||||
// {
|
||||
// new RadarrConfiguration { InstanceName = "radarr1" },
|
||||
// new RadarrConfiguration { InstanceName = "radarr2" },
|
||||
// new RadarrConfiguration { InstanceName = "radarr2" },
|
||||
// new RadarrConfiguration { InstanceName = "radarr3" },
|
||||
// new SonarrConfiguration { InstanceName = "sonarr1" },
|
||||
// new SonarrConfiguration { InstanceName = "sonarr1" },
|
||||
// };
|
||||
//
|
||||
// var result = configs.GetDuplicateInstanceNames();
|
||||
//
|
||||
// result.Should().BeEquivalentTo("radarr2", "sonarr1");
|
||||
// }
|
||||
// }
|
||||
|
@ -0,0 +1,151 @@
|
||||
using Recyclarr.Config.Filtering;
|
||||
using Recyclarr.Config.Parsing;
|
||||
using Recyclarr.TestLibrary;
|
||||
using Recyclarr.TrashGuide;
|
||||
|
||||
namespace Recyclarr.Tests.Config.Filtering;
|
||||
|
||||
[TestFixture]
|
||||
public class ConfigFiltersTest : IntegrationTestFixture
|
||||
{
|
||||
[Test]
|
||||
public void Filter_out_invalid_instances()
|
||||
{
|
||||
var sut = Resolve<InvalidInstancesFilter>();
|
||||
|
||||
var config = new RadarrConfigYaml { BaseUrl = "http://localhost:7878", ApiKey = "" };
|
||||
|
||||
var context = new FilterContext();
|
||||
|
||||
var result = sut.Filter(
|
||||
new ConfigFilterCriteria { Instances = ["instance1"] },
|
||||
[new LoadedConfigYaml("instance1", SupportedServices.Radarr, config)],
|
||||
context
|
||||
);
|
||||
|
||||
result.Should().BeEmpty();
|
||||
|
||||
var subject = context
|
||||
.Results.Should()
|
||||
.ContainSingle()
|
||||
.Which.Should()
|
||||
.BeOfType<InvalidInstancesFilterResult>()
|
||||
.Which.InvalidInstances.Should()
|
||||
.ContainSingle()
|
||||
.Subject;
|
||||
|
||||
subject.InstanceName.Should().Be("instance1");
|
||||
subject.Failures.Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Filter_out_split_instances()
|
||||
{
|
||||
var sut = Resolve<SplitInstancesFilter>();
|
||||
|
||||
var context = new FilterContext();
|
||||
|
||||
var result = sut.Filter(
|
||||
new ConfigFilterCriteria { Instances = ["instance1"] },
|
||||
[
|
||||
new LoadedConfigYaml(
|
||||
"instance1",
|
||||
SupportedServices.Radarr,
|
||||
new RadarrConfigYaml { BaseUrl = "http://same" }
|
||||
),
|
||||
new LoadedConfigYaml(
|
||||
"instance2",
|
||||
SupportedServices.Radarr,
|
||||
new RadarrConfigYaml { BaseUrl = "http://same" }
|
||||
),
|
||||
],
|
||||
context
|
||||
);
|
||||
|
||||
result.Should().BeEmpty();
|
||||
|
||||
var subject = context
|
||||
.Results.Should()
|
||||
.ContainSingle()
|
||||
.Which.Should()
|
||||
.BeOfType<SplitInstancesFilterResult>()
|
||||
.Which.SplitInstances.Should()
|
||||
.ContainSingle()
|
||||
.Subject;
|
||||
|
||||
subject.BaseUrl.Should().Be("http://same");
|
||||
subject.InstanceNames.Should().BeEquivalentTo("instance1", "instance2");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Filter_out_non_existent_instances()
|
||||
{
|
||||
var sut = Resolve<NonExistentInstancesFilter>();
|
||||
|
||||
var context = new FilterContext();
|
||||
LoadedConfigYaml[] yaml =
|
||||
[
|
||||
new(
|
||||
"instance1",
|
||||
SupportedServices.Radarr,
|
||||
new RadarrConfigYaml { BaseUrl = "http://myradarr.domain.com" }
|
||||
),
|
||||
];
|
||||
|
||||
var result = sut.Filter(
|
||||
new ConfigFilterCriteria { Instances = ["instance_non_existent"] },
|
||||
yaml,
|
||||
context
|
||||
);
|
||||
|
||||
result.Should().BeEquivalentTo(yaml);
|
||||
|
||||
var subject = context
|
||||
.Results.Should()
|
||||
.ContainSingle()
|
||||
.Which.Should()
|
||||
.BeOfType<NonExistentInstancesFilterResult>()
|
||||
.Which.NonExistentInstances.Should()
|
||||
.ContainSingle()
|
||||
.Subject;
|
||||
|
||||
subject.Should().Be("instance_non_existent");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Filter_out_duplicate_instances()
|
||||
{
|
||||
var sut = Resolve<DuplicateInstancesFilter>();
|
||||
|
||||
var context = new FilterContext();
|
||||
LoadedConfigYaml[] yaml =
|
||||
[
|
||||
new(
|
||||
"instance1",
|
||||
SupportedServices.Radarr,
|
||||
new RadarrConfigYaml { BaseUrl = "http://different2" }
|
||||
),
|
||||
new(
|
||||
"instance1",
|
||||
SupportedServices.Sonarr,
|
||||
new RadarrConfigYaml { BaseUrl = "http://different1" }
|
||||
),
|
||||
];
|
||||
|
||||
var result = sut.Filter(
|
||||
new ConfigFilterCriteria { Instances = ["instance1"] },
|
||||
yaml,
|
||||
context
|
||||
);
|
||||
|
||||
result.Should().BeEmpty();
|
||||
|
||||
context
|
||||
.Results.Should()
|
||||
.ContainSingle()
|
||||
.Which.Should()
|
||||
.BeOfType<DuplicateInstancesFilterResult>()
|
||||
.Which.DuplicateInstances.Should()
|
||||
.BeEquivalentTo("instance1");
|
||||
}
|
||||
}
|
Loading…
Reference in new issue