feat: Add config list templates command

Lists template YAML files in the trash repo.
pull/201/head
Robert Dailey 1 year ago committed by Robert Dailey
parent 902fbad4bf
commit a3c172cf02

@ -14,6 +14,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
existing CFs that Recyclarr never created in the first place. The default is `true`.
- New `quality_profiles` section supported for specifying information about quality profiles. For
now, this section doesn't do much, but paves the way for quality profile syncing.
- New CLI command: `config list` which lists information about current or available configuration
files.
- New `--templates` argument added to `config list` which will list available configuration YAML
templates that can be used in the Trash repo.
### Changed

@ -0,0 +1,20 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="config list templates" 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="config list templates" />
<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>

@ -2,12 +2,17 @@
"json_paths": {
"radarr": {
"custom_formats": ["docs/json/radarr/cf"],
"qualities": ["docs/json/radarr/quality-size"]
"qualities": ["docs/json/radarr/quality-size"],
"naming": ["docs/json/radarr/naming"]
},
"sonarr": {
"release_profiles": ["docs/json/sonarr/rp"],
"custom_formats": ["docs/json/sonarr/cf"],
"qualities": ["docs/json/sonarr/quality-size"]
"qualities": ["docs/json/sonarr/quality-size"],
"naming": ["docs/json/sonarr/naming"]
}
},
"recyclarr": {
"templates": "docs/recyclarr-configs"
}
}

@ -4,7 +4,7 @@ using Autofac;
using Autofac.Features.ResolveAnything;
using Recyclarr.Common;
using Recyclarr.Common.TestLibrary;
using Recyclarr.TestLibrary;
using Recyclarr.TestLibrary.Autofac;
using Recyclarr.TrashLib;
using Recyclarr.TrashLib.ApiServices.System;
using Recyclarr.TrashLib.Repo.VersionControl;

@ -26,6 +26,7 @@ public static class CliSetup
{
config.SetDescription("Operations for configuration files");
config.AddCommand<ConfigCreateCommand>("create");
config.AddCommand<ConfigListCommand>("list");
});
// LEGACY / DEPRECATED SUBCOMMANDS

@ -0,0 +1,55 @@
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using JetBrains.Annotations;
using Recyclarr.Cli.Console.Helpers;
using Recyclarr.TrashLib.Config.Listers;
using Recyclarr.TrashLib.ExceptionTypes;
using Recyclarr.TrashLib.Processors;
using Recyclarr.TrashLib.Repo;
using Spectre.Console.Cli;
namespace Recyclarr.Cli.Console.Commands;
[UsedImplicitly]
[Description("List configuration files in various ways.")]
public class ConfigListCommand : AsyncCommand<ConfigListCommand.CliSettings>
{
private readonly ILogger _log;
private readonly ConfigListProcessor _processor;
private readonly IRepoUpdater _repoUpdater;
[SuppressMessage("Design", "CA1034:Nested types should not be visible")]
public class CliSettings : BaseCommandSettings
{
[CommandArgument(0, "[ListCategory]")]
[EnumDescription<ConfigListCategory>("The type of configuration information to list.")]
public ConfigListCategory ListCategory { get; [UsedImplicitly] init; } = ConfigListCategory.Local;
}
public ConfigListCommand(ILogger log, ConfigListProcessor processor, IRepoUpdater repoUpdater)
{
_log = log;
_processor = processor;
_repoUpdater = repoUpdater;
}
public override async Task<int> ExecuteAsync(CommandContext context, CliSettings settings)
{
await _repoUpdater.UpdateRepo();
try
{
_processor.Process(settings.ListCategory);
}
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;
}
}

@ -23,11 +23,29 @@ public static class CommonMockFileSystemExtensions
fs.AddFileFromEmbeddedResource(path, typeInAssembly, $"{resourceSubPath}.{path.Name}");
}
public static void AddSameFileFromEmbeddedResource(
this MockFileSystem fs,
string path,
Type typeInAssembly,
string resourceSubPath = "Data")
{
fs.AddFileFromEmbeddedResource(fs.FileInfo.New(path), typeInAssembly, resourceSubPath);
}
public static void AddFileFromEmbeddedResource(
this MockFileSystem fs,
IFileInfo path,
Type typeInAssembly,
string embeddedResourcePath)
{
fs.AddFileFromEmbeddedResource(path.FullName, typeInAssembly, embeddedResourcePath);
}
public static void AddFileFromEmbeddedResource(
this MockFileSystem fs,
string path,
Type typeInAssembly,
string embeddedResourcePath)
{
var resourcePath = $"{typeInAssembly.Namespace}.{embeddedResourcePath}";
fs.AddFileFromEmbeddedResource(path, typeInAssembly.Assembly, resourcePath);

@ -1,6 +1,6 @@
using Autofac;
namespace Recyclarr.TestLibrary;
namespace Recyclarr.TestLibrary.Autofac;
public static class AutofacTestExtensions
{

@ -0,0 +1,22 @@
using System.Diagnostics.CodeAnalysis;
using Autofac.Features.Indexed;
namespace Recyclarr.TestLibrary.Autofac;
public class StubAutofacIndex<TKey, TValue> : IIndex<TKey, TValue>
where TKey : notnull
{
private readonly Dictionary<TKey, TValue> _values = new();
public void Add(TKey key, TValue value)
{
_values.Add(key, value);
}
public bool TryGetValue(TKey key, [UnscopedRef] out TValue value)
{
return _values.TryGetValue(key, out value!);
}
public TValue this[TKey key] => _values[key];
}

@ -0,0 +1,23 @@
using Autofac.Features.Indexed;
using Recyclarr.Cli.TestLibrary;
using Recyclarr.TrashLib.Config.Listers;
namespace Recyclarr.TrashLib.Tests.Config;
[TestFixture]
[Parallelizable(ParallelScope.All)]
public class ConfigAutofacModuleTest : IntegrationFixture
{
private static IEnumerable<ConfigListCategory> AllConfigListCategories()
{
return Enum.GetValues<ConfigListCategory>();
}
[TestCaseSource(nameof(AllConfigListCategories))]
public void All_list_category_types_registered(ConfigListCategory category)
{
var sut = Resolve<IIndex<ConfigListCategory, IConfigLister>>();
var result = sut.TryGetValue(category, out _);
result.Should().BeTrue();
}
}

@ -0,0 +1,33 @@
using System.IO.Abstractions;
using Recyclarr.Cli.TestLibrary;
using Recyclarr.TrashLib.Config;
using Recyclarr.TrashLib.Config.Listers;
using Recyclarr.TrashLib.Config.Services;
using Spectre.Console.Testing;
namespace Recyclarr.TrashLib.Tests.Config.Listers;
[TestFixture]
[Parallelizable(ParallelScope.All)]
public class ConfigTemplateListerTest : IntegrationFixture
{
[Test, AutoMockData]
public void Hidden_templates_are_not_rendered(
IFileInfo stubFile,
[Frozen(Matching.ImplementedInterfaces)] TestConsole console,
[Frozen] IConfigTemplateGuideService guideService,
ConfigTemplateLister sut)
{
guideService.TemplateData.Returns(new[]
{
new TemplatePath(SupportedServices.Radarr, "r1", stubFile, false),
new TemplatePath(SupportedServices.Radarr, "r2", stubFile, false),
new TemplatePath(SupportedServices.Sonarr, "s1", stubFile, false),
new TemplatePath(SupportedServices.Sonarr, "s2", stubFile, true),
});
sut.List();
console.Output.Should().NotContain("s2");
}
}

@ -6,7 +6,7 @@ using FluentValidation;
using Recyclarr.Cli.TestLibrary;
using Recyclarr.Common;
using Recyclarr.Common.Extensions;
using Recyclarr.TestLibrary;
using Recyclarr.TestLibrary.Autofac;
using Recyclarr.TrashLib.Config.Parsing;
using Recyclarr.TrashLib.Config.Services.Sonarr;
using Recyclarr.TrashLib.Config.Yaml;

@ -0,0 +1,52 @@
using System.IO.Abstractions;
using Recyclarr.Cli.TestLibrary;
using Recyclarr.Common.Extensions;
using Recyclarr.Common.TestLibrary;
using Recyclarr.TestLibrary;
using Recyclarr.TrashLib.Config;
using Recyclarr.TrashLib.Config.Services;
namespace Recyclarr.TrashLib.Tests.Config.Services;
[TestFixture]
[Parallelizable(ParallelScope.All)]
public class ConfigTemplateGuideServiceTest : IntegrationFixture
{
[Test, AutoMockData]
public void Throw_when_templates_dir_does_not_exist(
ConfigTemplateGuideService sut)
{
var act = () => _ = sut.TemplateData;
act.Should().Throw<InvalidDataException>().WithMessage("Path*templates*");
}
[Test]
public void Normal_behavior()
{
var templateDir = Paths.RepoDirectory.SubDir("docs/recyclarr-configs");
Fs.AddSameFileFromEmbeddedResource(templateDir.File("templates.json"), typeof(ConfigTemplateGuideServiceTest));
TemplatePath MakeTemplatePath(SupportedServices service, string id, string path)
{
var fsPath = templateDir.File(path);
Fs.AddEmptyFile(fsPath);
fsPath.Refresh();
return new TemplatePath(service, id, fsPath, false);
}
var expectedPaths = new[]
{
MakeTemplatePath(SupportedServices.Radarr, "hd-bluray-web", "radarr/hd-bluray-web.yml"),
MakeTemplatePath(SupportedServices.Radarr, "uhd-bluray-web", "radarr/uhd-bluray-web.yml"),
MakeTemplatePath(SupportedServices.Sonarr, "web-1080p-v4", "sonarr/web-1080p-v4.yml")
};
var sut = Resolve<ConfigTemplateGuideService>();
var data = sut.TemplateData;
data.Should().BeEquivalentTo(expectedPaths, o => o.Excluding(x => x.TemplateFile));
data.Select(x => x.TemplateFile.FullName)
.Should().BeEquivalentTo(expectedPaths.Select(x => x.TemplateFile.FullName));
}
}

@ -0,0 +1,18 @@
{
"radarr": [
{
"template": "radarr/hd-bluray-web.yml",
"id": "hd-bluray-web"
},
{
"template": "radarr/uhd-bluray-web.yml",
"id": "uhd-bluray-web"
}
],
"sonarr": [
{
"template": "sonarr/web-1080p-v4.yml",
"id": "web-1080p-v4"
}
]
}

@ -0,0 +1,25 @@
using Recyclarr.TestLibrary.Autofac;
using Recyclarr.TrashLib.Config.Listers;
using Recyclarr.TrashLib.Processors;
namespace Recyclarr.TrashLib.Tests.Processors;
[TestFixture]
[Parallelizable(ParallelScope.All)]
public class ConfigListProcessorTest
{
[Test]
[InlineAutoMockData(ConfigListCategory.Templates)]
public void List_templates_invokes_correct_lister(
ConfigListCategory category,
[Frozen(Matching.ImplementedInterfaces)] StubAutofacIndex<ConfigListCategory, IConfigLister> configListers,
IConfigLister lister,
ConfigListProcessor sut)
{
configListers.Add(category, lister);
sut.Process(category);
lister.Received().List();
}
}

@ -3,7 +3,7 @@ using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using Recyclarr.Cli.TestLibrary;
using Recyclarr.TestLibrary;
using Recyclarr.TestLibrary.Autofac;
using Recyclarr.TrashLib.Compatibility.Sonarr;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Pipelines.ReleaseProfile.Api;

@ -1,8 +1,10 @@
using System.Reflection;
using Autofac;
using FluentValidation;
using Recyclarr.TrashLib.Config.Listers;
using Recyclarr.TrashLib.Config.Parsing;
using Recyclarr.TrashLib.Config.Secrets;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Config.Settings;
using Recyclarr.TrashLib.Config.Yaml;
using YamlDotNet.Serialization;
@ -15,7 +17,7 @@ public class ConfigAutofacModule : Module
{
private readonly Assembly[] _assemblies;
public ConfigAutofacModule(Assembly[] assemblies)
public ConfigAutofacModule(params Assembly[] assemblies)
{
_assemblies = assemblies;
}
@ -39,5 +41,10 @@ public class ConfigAutofacModule : Module
builder.RegisterType<ConfigurationFinder>().As<IConfigurationFinder>();
builder.RegisterType<ConfigValidationExecutor>();
builder.RegisterType<ConfigParser>();
builder.RegisterType<ConfigTemplateGuideService>().As<IConfigTemplateGuideService>();
// Config Listers
builder.RegisterType<ConfigTemplateLister>().Keyed<IConfigLister>(ConfigListCategory.Templates);
builder.RegisterType<ConfigLocalLister>().Keyed<IConfigLister>(ConfigListCategory.Local);
}
}

@ -0,0 +1,7 @@
namespace Recyclarr.TrashLib.Config.Listers;
public enum ConfigListCategory
{
Local,
Templates
}

@ -0,0 +1,18 @@
using Spectre.Console;
namespace Recyclarr.TrashLib.Config.Listers;
public class ConfigLocalLister : IConfigLister
{
private readonly IAnsiConsole _console;
public ConfigLocalLister(IAnsiConsole console)
{
_console = console;
}
public void List()
{
_console.Write("Local listing is not supported yet, but coming soon.");
}
}

@ -0,0 +1,52 @@
using MoreLinq;
using Recyclarr.TrashLib.Config.Services;
using Spectre.Console;
namespace Recyclarr.TrashLib.Config.Listers;
public class ConfigTemplateLister : IConfigLister
{
private readonly IAnsiConsole _console;
private readonly IConfigTemplateGuideService _guideService;
public ConfigTemplateLister(IAnsiConsole console, IConfigTemplateGuideService guideService)
{
_console = console;
_guideService = guideService;
}
public void List()
{
var data = _guideService.TemplateData;
var table = new Table();
var empty = new Markup("");
var sonarrRowItems = RenderTemplates(table, data, SupportedServices.Sonarr);
var radarrRowItems = RenderTemplates(table, data, SupportedServices.Radarr);
var items = radarrRowItems
.ZipLongest(sonarrRowItems, (l, r) => (l ?? empty, r ?? empty));
foreach (var (r, s) in items)
{
table.AddRow(r, s);
}
_console.Write(table);
}
private static IEnumerable<Markup> RenderTemplates(
Table table,
IEnumerable<TemplatePath> templatePaths,
SupportedServices service)
{
var paths = templatePaths
.Where(x => x.Service == service && !x.Hidden)
.Select(x => Markup.FromInterpolated($"[blue]{x.Id}[/]"))
.ToList();
table.AddColumn(service.ToString());
return paths;
}
}

@ -0,0 +1,6 @@
namespace Recyclarr.TrashLib.Config.Listers;
public interface IConfigLister
{
void List();
}

@ -114,7 +114,6 @@ public class ConfigurationLoader : IConfigurationLoader
if (!ParseSingleSection(parser))
{
parser.SkipThisAndNestedEvents();
continue;
}
}
}

@ -0,0 +1,61 @@
using System.Collections.ObjectModel;
using System.IO.Abstractions;
using JetBrains.Annotations;
using Recyclarr.Common.Extensions;
using Recyclarr.TrashLib.Repo;
using Recyclarr.TrashLib.Startup;
namespace Recyclarr.TrashLib.Config.Services;
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
public record TemplateEntry(string Id, string Template, bool Hidden = false);
public record TemplatesData
{
public ReadOnlyCollection<TemplateEntry> Radarr { get; [UsedImplicitly] init; } = new(Array.Empty<TemplateEntry>());
public ReadOnlyCollection<TemplateEntry> Sonarr { get; [UsedImplicitly] init; } = new(Array.Empty<TemplateEntry>());
}
public record TemplatePath(SupportedServices Service, string Id, IFileInfo TemplateFile, bool Hidden);
public class ConfigTemplateGuideService : IConfigTemplateGuideService
{
private readonly IRepoMetadataBuilder _metadataBuilder;
private readonly IAppPaths _paths;
private readonly Lazy<IReadOnlyCollection<TemplatePath>> _templateData;
public ConfigTemplateGuideService(
IRepoMetadataBuilder metadataBuilder,
IAppPaths paths)
{
_metadataBuilder = metadataBuilder;
_paths = paths;
_templateData = new Lazy<IReadOnlyCollection<TemplatePath>>(LoadTemplateData);
}
private IReadOnlyCollection<TemplatePath> LoadTemplateData()
{
var metadata = _metadataBuilder.GetMetadata();
var templatesPath = _paths.RepoDirectory.SubDir(metadata.Recyclarr.Templates);
if (!templatesPath.Exists)
{
throw new InvalidDataException(
$"Path to recyclarr templates does not exist: {metadata.Recyclarr.Templates}");
}
var templates = TrashRepoJsonParser.Deserialize<TemplatesData>(templatesPath.File("templates.json"));
TemplatePath NewTemplatePath(TemplateEntry entry, SupportedServices service)
{
return new TemplatePath(service, entry.Id, templatesPath.File(entry.Template), entry.Hidden);
}
return templates.Radarr
.Select(x => NewTemplatePath(x, SupportedServices.Radarr))
.Concat(templates.Sonarr.Select(x => NewTemplatePath(x, SupportedServices.Sonarr)))
.ToList();
}
public IReadOnlyCollection<TemplatePath> TemplateData => _templateData.Value;
}

@ -0,0 +1,6 @@
namespace Recyclarr.TrashLib.Config.Services;
public interface IConfigTemplateGuideService
{
IReadOnlyCollection<TemplatePath> TemplateData { get; }
}

@ -0,0 +1,27 @@
using Autofac.Features.Indexed;
using Recyclarr.TrashLib.Config.Listers;
namespace Recyclarr.TrashLib.Processors;
public class ConfigListProcessor
{
private readonly ILogger _log;
private readonly IIndex<ConfigListCategory, IConfigLister> _configListers;
public ConfigListProcessor(ILogger log, IIndex<ConfigListCategory, IConfigLister> configListers)
{
_log = log;
_configListers = configListers;
}
public void Process(ConfigListCategory listCategory)
{
_log.Debug("Listing configuration for category {Category}", listCategory);
if (!_configListers.TryGetValue(listCategory, out var lister))
{
throw new ArgumentOutOfRangeException(nameof(listCategory), listCategory, "Unknown list category");
}
lister.List();
}
}

@ -10,5 +10,6 @@ public class ServiceProcessorsAutofacModule : Module
builder.RegisterType<ConfigCreationProcessor>().As<IConfigCreationProcessor>();
builder.RegisterType<SyncProcessor>().As<ISyncProcessor>();
builder.RegisterType<SyncPipelineExecutor>();
builder.RegisterType<ConfigListProcessor>();
}
}

@ -1,6 +0,0 @@
namespace Recyclarr.TrashLib.Repo;
public interface IRepoMetadataParser
{
RepoMetadata Deserialize();
}

@ -8,7 +8,6 @@ public class RepoAutofacModule : Module
{
base.Load(builder);
builder.RegisterType<RepoUpdater>().As<IRepoUpdater>();
builder.RegisterType<RepoMetadataParser>().As<IRepoMetadataParser>();
builder.RegisterType<RepoMetadataBuilder>().As<IRepoMetadataBuilder>().InstancePerLifetimeScope();
}
}

@ -19,7 +19,13 @@ public record JsonPaths
public SonarrMetadata Sonarr { get; init; } = new();
}
public record RecyclarrMetadata
{
public string Templates { get; init; } = "";
}
public record RepoMetadata
{
public JsonPaths JsonPaths { get; init; } = new();
public RecyclarrMetadata Recyclarr { get; init; } = new();
}

@ -8,12 +8,11 @@ public class RepoMetadataBuilder : IRepoMetadataBuilder
private readonly IAppPaths _paths;
private readonly Lazy<RepoMetadata> _metadata;
public RepoMetadataBuilder(
IRepoMetadataParser parser,
IAppPaths paths)
public RepoMetadataBuilder(IAppPaths paths)
{
_paths = paths;
_metadata = new Lazy<RepoMetadata>(parser.Deserialize);
_metadata = new Lazy<RepoMetadata>(()
=> TrashRepoJsonParser.Deserialize<RepoMetadata>(_paths.RepoDirectory.File("metadata.json")));
}
public IReadOnlyList<IDirectoryInfo> ToDirectoryInfoList(IEnumerable<string> listOfDirectories)

@ -1,38 +0,0 @@
using System.IO.Abstractions;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Recyclarr.TrashLib.Startup;
namespace Recyclarr.TrashLib.Repo;
public class RepoMetadataParser : IRepoMetadataParser
{
private readonly IAppPaths _paths;
public RepoMetadataParser(IAppPaths paths)
{
_paths = paths;
}
public RepoMetadata Deserialize()
{
var serializer = JsonSerializer.Create(new JsonSerializerSettings
{
ContractResolver = new DefaultContractResolver
{
NamingStrategy = new SnakeCaseNamingStrategy()
}
});
var metadataFile = _paths.RepoDirectory.File("metadata.json");
using var stream = new JsonTextReader(metadataFile.OpenText());
var metadata = serializer.Deserialize<RepoMetadata>(stream);
if (metadata is null)
{
throw new InvalidDataException("Unable to deserialize metadata.json");
}
return metadata;
}
}

@ -0,0 +1,29 @@
using System.IO.Abstractions;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
namespace Recyclarr.TrashLib.Repo;
public static class TrashRepoJsonParser
{
public static T Deserialize<T>(IFileInfo jsonFile)
{
var serializer = JsonSerializer.Create(new JsonSerializerSettings
{
ContractResolver = new DefaultContractResolver
{
NamingStrategy = new SnakeCaseNamingStrategy()
}
});
using var stream = new JsonTextReader(jsonFile.OpenText());
var obj = serializer.Deserialize<T>(stream);
if (obj is null)
{
throw new InvalidDataException($"Unable to deserialize {jsonFile}");
}
return obj;
}
}

@ -112,6 +112,7 @@
&amp;lt;/Language&amp;gt;&#xD;
&amp;lt;/profile&amp;gt;&lt;/RIDER_SETTINGS&gt;&lt;CSharpFormatDocComments&gt;True&lt;/CSharpFormatDocComments&gt;&lt;XAMLCollapseEmptyTags&gt;False&lt;/XAMLCollapseEmptyTags&gt;&lt;RemoveCodeRedundancies&gt;True&lt;/RemoveCodeRedundancies&gt;&lt;CSMakeFieldReadonly&gt;True&lt;/CSMakeFieldReadonly&gt;&lt;/Profile&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/CodeCleanup/SilentCleanupProfile/@EntryValue">Recyclarr Cleanup</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Listers/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Persister/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=radarr/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Recyclarr/@EntryIndexedValue">True</s:Boolean>

Loading…
Cancel
Save