diff --git a/CHANGELOG.md b/CHANGELOG.md index 17893a1c..2a167bb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/.idea/.idea.Recyclarr/.idea/runConfigurations/config_list_templates.xml b/src/.idea/.idea.Recyclarr/.idea/runConfigurations/config_list_templates.xml new file mode 100644 index 00000000..c696e2c1 --- /dev/null +++ b/src/.idea/.idea.Recyclarr/.idea/runConfigurations/config_list_templates.xml @@ -0,0 +1,20 @@ + + + + \ No newline at end of file diff --git a/src/Recyclarr.Cli.TestLibrary/Data/metadata.json b/src/Recyclarr.Cli.TestLibrary/Data/metadata.json index 326a8299..7338b37a 100644 --- a/src/Recyclarr.Cli.TestLibrary/Data/metadata.json +++ b/src/Recyclarr.Cli.TestLibrary/Data/metadata.json @@ -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" } } diff --git a/src/Recyclarr.Cli.TestLibrary/IntegrationFixture.cs b/src/Recyclarr.Cli.TestLibrary/IntegrationFixture.cs index 26ee8912..889be4ba 100644 --- a/src/Recyclarr.Cli.TestLibrary/IntegrationFixture.cs +++ b/src/Recyclarr.Cli.TestLibrary/IntegrationFixture.cs @@ -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; diff --git a/src/Recyclarr.Cli/Console/CliSetup.cs b/src/Recyclarr.Cli/Console/CliSetup.cs index e2c69979..4e27b633 100644 --- a/src/Recyclarr.Cli/Console/CliSetup.cs +++ b/src/Recyclarr.Cli/Console/CliSetup.cs @@ -26,6 +26,7 @@ public static class CliSetup { config.SetDescription("Operations for configuration files"); config.AddCommand("create"); + config.AddCommand("list"); }); // LEGACY / DEPRECATED SUBCOMMANDS diff --git a/src/Recyclarr.Cli/Console/Commands/ConfigListCommand.cs b/src/Recyclarr.Cli/Console/Commands/ConfigListCommand.cs new file mode 100644 index 00000000..3468ae02 --- /dev/null +++ b/src/Recyclarr.Cli/Console/Commands/ConfigListCommand.cs @@ -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 +{ + 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("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 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; + } +} diff --git a/src/Recyclarr.Common.TestLibrary/CommonMockFileSystemExtensions.cs b/src/Recyclarr.Common.TestLibrary/CommonMockFileSystemExtensions.cs index 30214756..e7a9870e 100644 --- a/src/Recyclarr.Common.TestLibrary/CommonMockFileSystemExtensions.cs +++ b/src/Recyclarr.Common.TestLibrary/CommonMockFileSystemExtensions.cs @@ -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); diff --git a/src/Recyclarr.TestLibrary/AutofacTestExtensions.cs b/src/Recyclarr.TestLibrary/Autofac/AutofacTestExtensions.cs similarity index 91% rename from src/Recyclarr.TestLibrary/AutofacTestExtensions.cs rename to src/Recyclarr.TestLibrary/Autofac/AutofacTestExtensions.cs index 9e6ed383..63cf7d10 100644 --- a/src/Recyclarr.TestLibrary/AutofacTestExtensions.cs +++ b/src/Recyclarr.TestLibrary/Autofac/AutofacTestExtensions.cs @@ -1,6 +1,6 @@ using Autofac; -namespace Recyclarr.TestLibrary; +namespace Recyclarr.TestLibrary.Autofac; public static class AutofacTestExtensions { diff --git a/src/Recyclarr.TestLibrary/Autofac/StubAutofacIndex.cs b/src/Recyclarr.TestLibrary/Autofac/StubAutofacIndex.cs new file mode 100644 index 00000000..d3253376 --- /dev/null +++ b/src/Recyclarr.TestLibrary/Autofac/StubAutofacIndex.cs @@ -0,0 +1,22 @@ +using System.Diagnostics.CodeAnalysis; +using Autofac.Features.Indexed; + +namespace Recyclarr.TestLibrary.Autofac; + +public class StubAutofacIndex : IIndex + where TKey : notnull +{ + private readonly Dictionary _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]; +} diff --git a/src/Recyclarr.TrashLib.Tests/Config/ConfigAutofacModuleTest.cs b/src/Recyclarr.TrashLib.Tests/Config/ConfigAutofacModuleTest.cs new file mode 100644 index 00000000..e86f6c7a --- /dev/null +++ b/src/Recyclarr.TrashLib.Tests/Config/ConfigAutofacModuleTest.cs @@ -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 AllConfigListCategories() + { + return Enum.GetValues(); + } + + [TestCaseSource(nameof(AllConfigListCategories))] + public void All_list_category_types_registered(ConfigListCategory category) + { + var sut = Resolve>(); + var result = sut.TryGetValue(category, out _); + result.Should().BeTrue(); + } +} diff --git a/src/Recyclarr.TrashLib.Tests/Config/Listers/ConfigTemplateListerTest.cs b/src/Recyclarr.TrashLib.Tests/Config/Listers/ConfigTemplateListerTest.cs new file mode 100644 index 00000000..2c47d4d2 --- /dev/null +++ b/src/Recyclarr.TrashLib.Tests/Config/Listers/ConfigTemplateListerTest.cs @@ -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"); + } +} diff --git a/src/Recyclarr.TrashLib.Tests/Config/Parsing/ConfigurationLoaderTest.cs b/src/Recyclarr.TrashLib.Tests/Config/Parsing/ConfigurationLoaderTest.cs index a69030f2..bb1a0901 100644 --- a/src/Recyclarr.TrashLib.Tests/Config/Parsing/ConfigurationLoaderTest.cs +++ b/src/Recyclarr.TrashLib.Tests/Config/Parsing/ConfigurationLoaderTest.cs @@ -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; diff --git a/src/Recyclarr.TrashLib.Tests/Config/Services/ConfigTemplateGuideServiceTest.cs b/src/Recyclarr.TrashLib.Tests/Config/Services/ConfigTemplateGuideServiceTest.cs new file mode 100644 index 00000000..c5a84412 --- /dev/null +++ b/src/Recyclarr.TrashLib.Tests/Config/Services/ConfigTemplateGuideServiceTest.cs @@ -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().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(); + + 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)); + } +} diff --git a/src/Recyclarr.TrashLib.Tests/Config/Services/Data/templates.json b/src/Recyclarr.TrashLib.Tests/Config/Services/Data/templates.json new file mode 100644 index 00000000..e143a8ed --- /dev/null +++ b/src/Recyclarr.TrashLib.Tests/Config/Services/Data/templates.json @@ -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" + } + ] +} diff --git a/src/Recyclarr.TrashLib.Tests/Processors/ConfigListProcessorTest.cs b/src/Recyclarr.TrashLib.Tests/Processors/ConfigListProcessorTest.cs new file mode 100644 index 00000000..9d887b33 --- /dev/null +++ b/src/Recyclarr.TrashLib.Tests/Processors/ConfigListProcessorTest.cs @@ -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 configListers, + IConfigLister lister, + ConfigListProcessor sut) + { + configListers.Add(category, lister); + + sut.Process(category); + + lister.Received().List(); + } +} diff --git a/src/Recyclarr.TrashLib.Tests/Sonarr/SonarrCompatibilityTest.cs b/src/Recyclarr.TrashLib.Tests/Sonarr/SonarrCompatibilityTest.cs index 4caae2bf..c61bd5b5 100644 --- a/src/Recyclarr.TrashLib.Tests/Sonarr/SonarrCompatibilityTest.cs +++ b/src/Recyclarr.TrashLib.Tests/Sonarr/SonarrCompatibilityTest.cs @@ -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; diff --git a/src/Recyclarr.TrashLib/Config/ConfigAutofacModule.cs b/src/Recyclarr.TrashLib/Config/ConfigAutofacModule.cs index ea804719..7e87df54 100644 --- a/src/Recyclarr.TrashLib/Config/ConfigAutofacModule.cs +++ b/src/Recyclarr.TrashLib/Config/ConfigAutofacModule.cs @@ -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().As(); builder.RegisterType(); builder.RegisterType(); + builder.RegisterType().As(); + + // Config Listers + builder.RegisterType().Keyed(ConfigListCategory.Templates); + builder.RegisterType().Keyed(ConfigListCategory.Local); } } diff --git a/src/Recyclarr.TrashLib/Config/Listers/ConfigListCategory.cs b/src/Recyclarr.TrashLib/Config/Listers/ConfigListCategory.cs new file mode 100644 index 00000000..6e860461 --- /dev/null +++ b/src/Recyclarr.TrashLib/Config/Listers/ConfigListCategory.cs @@ -0,0 +1,7 @@ +namespace Recyclarr.TrashLib.Config.Listers; + +public enum ConfigListCategory +{ + Local, + Templates +} diff --git a/src/Recyclarr.TrashLib/Config/Listers/ConfigLocalLister.cs b/src/Recyclarr.TrashLib/Config/Listers/ConfigLocalLister.cs new file mode 100644 index 00000000..1a1f2817 --- /dev/null +++ b/src/Recyclarr.TrashLib/Config/Listers/ConfigLocalLister.cs @@ -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."); + } +} diff --git a/src/Recyclarr.TrashLib/Config/Listers/ConfigTemplateLister.cs b/src/Recyclarr.TrashLib/Config/Listers/ConfigTemplateLister.cs new file mode 100644 index 00000000..ab4bb679 --- /dev/null +++ b/src/Recyclarr.TrashLib/Config/Listers/ConfigTemplateLister.cs @@ -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 RenderTemplates( + Table table, + IEnumerable 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; + } +} diff --git a/src/Recyclarr.TrashLib/Config/Listers/IConfigLister.cs b/src/Recyclarr.TrashLib/Config/Listers/IConfigLister.cs new file mode 100644 index 00000000..411b193c --- /dev/null +++ b/src/Recyclarr.TrashLib/Config/Listers/IConfigLister.cs @@ -0,0 +1,6 @@ +namespace Recyclarr.TrashLib.Config.Listers; + +public interface IConfigLister +{ + void List(); +} diff --git a/src/Recyclarr.TrashLib/Config/Parsing/ConfigurationLoader.cs b/src/Recyclarr.TrashLib/Config/Parsing/ConfigurationLoader.cs index f1b52433..460158df 100644 --- a/src/Recyclarr.TrashLib/Config/Parsing/ConfigurationLoader.cs +++ b/src/Recyclarr.TrashLib/Config/Parsing/ConfigurationLoader.cs @@ -114,7 +114,6 @@ public class ConfigurationLoader : IConfigurationLoader if (!ParseSingleSection(parser)) { parser.SkipThisAndNestedEvents(); - continue; } } } diff --git a/src/Recyclarr.TrashLib/Config/Services/ConfigTemplateGuideService.cs b/src/Recyclarr.TrashLib/Config/Services/ConfigTemplateGuideService.cs new file mode 100644 index 00000000..3a3e7d4a --- /dev/null +++ b/src/Recyclarr.TrashLib/Config/Services/ConfigTemplateGuideService.cs @@ -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 Radarr { get; [UsedImplicitly] init; } = new(Array.Empty()); + public ReadOnlyCollection Sonarr { get; [UsedImplicitly] init; } = new(Array.Empty()); +} + +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> _templateData; + + public ConfigTemplateGuideService( + IRepoMetadataBuilder metadataBuilder, + IAppPaths paths) + { + _metadataBuilder = metadataBuilder; + _paths = paths; + _templateData = new Lazy>(LoadTemplateData); + } + + private IReadOnlyCollection 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(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 TemplateData => _templateData.Value; +} diff --git a/src/Recyclarr.TrashLib/Config/Services/IConfigTemplateGuideService.cs b/src/Recyclarr.TrashLib/Config/Services/IConfigTemplateGuideService.cs new file mode 100644 index 00000000..a6e12cd5 --- /dev/null +++ b/src/Recyclarr.TrashLib/Config/Services/IConfigTemplateGuideService.cs @@ -0,0 +1,6 @@ +namespace Recyclarr.TrashLib.Config.Services; + +public interface IConfigTemplateGuideService +{ + IReadOnlyCollection TemplateData { get; } +} diff --git a/src/Recyclarr.TrashLib/Processors/ConfigListProcessor.cs b/src/Recyclarr.TrashLib/Processors/ConfigListProcessor.cs new file mode 100644 index 00000000..3dc8a14c --- /dev/null +++ b/src/Recyclarr.TrashLib/Processors/ConfigListProcessor.cs @@ -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 _configListers; + + public ConfigListProcessor(ILogger log, IIndex 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(); + } +} diff --git a/src/Recyclarr.TrashLib/Processors/ServiceProcessorsAutofacModule.cs b/src/Recyclarr.TrashLib/Processors/ServiceProcessorsAutofacModule.cs index 106a6fb9..3cb19aa0 100644 --- a/src/Recyclarr.TrashLib/Processors/ServiceProcessorsAutofacModule.cs +++ b/src/Recyclarr.TrashLib/Processors/ServiceProcessorsAutofacModule.cs @@ -10,5 +10,6 @@ public class ServiceProcessorsAutofacModule : Module builder.RegisterType().As(); builder.RegisterType().As(); builder.RegisterType(); + builder.RegisterType(); } } diff --git a/src/Recyclarr.TrashLib/Repo/IRepoMetadataParser.cs b/src/Recyclarr.TrashLib/Repo/IRepoMetadataParser.cs deleted file mode 100644 index 644732ff..00000000 --- a/src/Recyclarr.TrashLib/Repo/IRepoMetadataParser.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Recyclarr.TrashLib.Repo; - -public interface IRepoMetadataParser -{ - RepoMetadata Deserialize(); -} diff --git a/src/Recyclarr.TrashLib/Repo/RepoAutofacModule.cs b/src/Recyclarr.TrashLib/Repo/RepoAutofacModule.cs index b2ae5156..151eb193 100644 --- a/src/Recyclarr.TrashLib/Repo/RepoAutofacModule.cs +++ b/src/Recyclarr.TrashLib/Repo/RepoAutofacModule.cs @@ -8,7 +8,6 @@ public class RepoAutofacModule : Module { base.Load(builder); builder.RegisterType().As(); - builder.RegisterType().As(); builder.RegisterType().As().InstancePerLifetimeScope(); } } diff --git a/src/Recyclarr.TrashLib/Repo/RepoMetadata.cs b/src/Recyclarr.TrashLib/Repo/RepoMetadata.cs index f44e23ac..f8ee7e7c 100644 --- a/src/Recyclarr.TrashLib/Repo/RepoMetadata.cs +++ b/src/Recyclarr.TrashLib/Repo/RepoMetadata.cs @@ -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(); } diff --git a/src/Recyclarr.TrashLib/Repo/RepoMetadataBuilder.cs b/src/Recyclarr.TrashLib/Repo/RepoMetadataBuilder.cs index 3c6c48bc..d7da876e 100644 --- a/src/Recyclarr.TrashLib/Repo/RepoMetadataBuilder.cs +++ b/src/Recyclarr.TrashLib/Repo/RepoMetadataBuilder.cs @@ -8,12 +8,11 @@ public class RepoMetadataBuilder : IRepoMetadataBuilder private readonly IAppPaths _paths; private readonly Lazy _metadata; - public RepoMetadataBuilder( - IRepoMetadataParser parser, - IAppPaths paths) + public RepoMetadataBuilder(IAppPaths paths) { _paths = paths; - _metadata = new Lazy(parser.Deserialize); + _metadata = new Lazy(() + => TrashRepoJsonParser.Deserialize(_paths.RepoDirectory.File("metadata.json"))); } public IReadOnlyList ToDirectoryInfoList(IEnumerable listOfDirectories) diff --git a/src/Recyclarr.TrashLib/Repo/RepoMetadataParser.cs b/src/Recyclarr.TrashLib/Repo/RepoMetadataParser.cs deleted file mode 100644 index 2fe062bb..00000000 --- a/src/Recyclarr.TrashLib/Repo/RepoMetadataParser.cs +++ /dev/null @@ -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(stream); - if (metadata is null) - { - throw new InvalidDataException("Unable to deserialize metadata.json"); - } - - return metadata; - } -} diff --git a/src/Recyclarr.TrashLib/Repo/TrashRepoJsonParser.cs b/src/Recyclarr.TrashLib/Repo/TrashRepoJsonParser.cs new file mode 100644 index 00000000..b753cfe0 --- /dev/null +++ b/src/Recyclarr.TrashLib/Repo/TrashRepoJsonParser.cs @@ -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(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(stream); + if (obj is null) + { + throw new InvalidDataException($"Unable to deserialize {jsonFile}"); + } + + return obj; + } +} diff --git a/src/Recyclarr.sln.DotSettings b/src/Recyclarr.sln.DotSettings index ca2044fd..13975a2f 100644 --- a/src/Recyclarr.sln.DotSettings +++ b/src/Recyclarr.sln.DotSettings @@ -112,6 +112,7 @@ &lt;/Language&gt; &lt;/profile&gt;</RIDER_SETTINGS><CSharpFormatDocComments>True</CSharpFormatDocComments><XAMLCollapseEmptyTags>False</XAMLCollapseEmptyTags><RemoveCodeRedundancies>True</RemoveCodeRedundancies><CSMakeFieldReadonly>True</CSMakeFieldReadonly></Profile> Recyclarr Cleanup + True True True True