feat: settings functionality

A settings file, `settings.yml` is now available which stores global
configuration such as the clone URL for the Github Trash repository.
pull/47/head
Robert Dailey 2 years ago
parent b6b6ebda9e
commit 2fe29f3b16

@ -8,6 +8,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- New settings file to control non-service specific behavior of Trash Updater. See [the
documentation](setref) for more information.
- Trash git repository URL can be overridden in settings.
[setref]: https://github.com/rcdailey/trash-updater/wiki/Settings-Reference
### Fixed
- Remove `System.Reactive.xml` from the published ZIP files.

@ -38,10 +38,11 @@
</PropertyGroup>
<ItemGroup Condition="$(ProjectName.EndsWith('.Tests'))">
<PackageReference Include="AutofacContrib.NSubstitute" PrivateAssets="All" />
<PackageReference Include="AutoFixture" PrivateAssets="All" />
<PackageReference Include="AutoFixture.AutoNSubstitute" PrivateAssets="All" />
<PackageReference Include="AutoFixture.NUnit3" PrivateAssets="All" />
<PackageReference Include="AutofacContrib.NSubstitute" PrivateAssets="All" />
<PackageReference Include="coverlet.collector" PrivateAssets="All" />
<PackageReference Include="FluentAssertions" PrivateAssets="All" />
<PackageReference Include="FluentAssertions.Json" PrivateAssets="All" />
<PackageReference Include="GitHubActionsTestLogger" PrivateAssets="All" />
@ -53,7 +54,7 @@
<PackageReference Include="NUnit3TestAdapter" PrivateAssets="All" />
<PackageReference Include="Serilog.Sinks.NUnit" PrivateAssets="All" />
<PackageReference Include="Serilog.Sinks.TestCorrelator" PrivateAssets="All" />
<PackageReference Include="coverlet.collector" PrivateAssets="All" />
<PackageReference Include="System.IO.Abstractions.TestingHelpers" PrivateAssets="All" />
</ItemGroup>
<ItemGroup Condition="$(ProjectName.EndsWith('.Tests'))">

@ -36,6 +36,7 @@
<PackageReference Update="Serilog.Sinks.TestCorrelator" Version="3.*" />
<PackageReference Update="System.Data.HashFunction.FNV" Version="2.*" />
<PackageReference Update="System.IO.Abstractions" Version="16.*" />
<PackageReference Update="System.IO.Abstractions.TestingHelpers" Version="16.*" />
<PackageReference Update="System.Reactive" Version="5.*" />
<PackageReference Update="YamlDotNet" Version="11.*" />
</ItemGroup>

@ -1,4 +1,3 @@
using System;
using System.Reflection;
using Autofac;
using AutoFixture.Kernel;

@ -3,7 +3,7 @@ using AutoFixture.NUnit3;
namespace TestLibrary.AutoFixture;
public class InlineAutoMockDataAttribute : InlineAutoDataAttribute
public sealed class InlineAutoMockDataAttribute : InlineAutoDataAttribute
{
[SuppressMessage("Design", "CA1019", MessageId = "Define accessors for attribute arguments",
Justification = "The parameter is forwarded to the base class and not used directly")]

@ -42,7 +42,7 @@ public class ConfigurationLoaderTest
{
var builder = new ContainerBuilder();
builder.RegisterType<DefaultObjectFactory>().As<IObjectFactory>();
builder.RegisterType<YamlDeserializerFactory>().As<IYamlDeserializerFactory>();
builder.RegisterType<YamlSerializerFactory>().As<IYamlSerializerFactory>();
return builder.Build();
}

@ -10,6 +10,8 @@ internal static class AppPaths
public static string DefaultConfigPath { get; } = Path.Combine(AppContext.BaseDirectory, "trash.yml");
public static string DefaultSettingsPath { get; } = Path.Combine(AppDataPath, "settings.yml");
public static string LogDirectory { get; } = Path.Combine(AppDataPath, "logs");
public static string RepoDirectory { get; } = Path.Combine(AppDataPath, "repo");

@ -6,8 +6,8 @@ using Flurl.Http;
using JetBrains.Annotations;
using Serilog;
using Serilog.Core;
using Trash.Command.Helpers;
using Trash.Config;
using TrashLib.Config.Settings;
using TrashLib.Radarr.Config;
using TrashLib.Radarr.CustomFormat;
using TrashLib.Radarr.QualityDefinition;
@ -27,10 +27,11 @@ public class RadarrCommand : ServiceCommand
ILogger log,
LoggingLevelSwitch loggingLevelSwitch,
ILogJanitor logJanitor,
ISettingsPersister settingsPersister,
IConfigurationLoader<RadarrConfiguration> configLoader,
Func<IRadarrQualityDefinitionUpdater> qualityUpdaterFactory,
Func<ICustomFormatUpdater> customFormatUpdaterFactory)
: base(log, loggingLevelSwitch, logJanitor)
: base(log, loggingLevelSwitch, logJanitor, settingsPersister)
{
_log = log;
_configLoader = configLoader;

@ -12,6 +12,7 @@ using Newtonsoft.Json;
using Serilog;
using Serilog.Core;
using Serilog.Events;
using TrashLib.Config.Settings;
using TrashLib.Extensions;
using YamlDotNet.Core;
@ -22,20 +23,24 @@ public abstract class ServiceCommand : ICommand, IServiceCommand
private readonly ILogger _log;
private readonly LoggingLevelSwitch _loggingLevelSwitch;
private readonly ILogJanitor _logJanitor;
private readonly ISettingsPersister _settingsPersister;
protected ServiceCommand(
ILogger log,
LoggingLevelSwitch loggingLevelSwitch,
ILogJanitor logJanitor)
ILogJanitor logJanitor,
ISettingsPersister settingsPersister)
{
_loggingLevelSwitch = loggingLevelSwitch;
_logJanitor = logJanitor;
_settingsPersister = settingsPersister;
_log = log;
}
public async ValueTask ExecuteAsync(IConsole console)
{
SetupLogging();
LoadSettings();
SetupHttp();
try
@ -65,6 +70,11 @@ public abstract class ServiceCommand : ICommand, IServiceCommand
}
}
private void LoadSettings()
{
_settingsPersister.Load();
}
[CommandOption("preview", 'p', Description =
"Only display the processed markdown results without making any API calls.")]
public bool Preview { get; [UsedImplicitly] set; } = false;

@ -6,8 +6,8 @@ using Flurl.Http;
using JetBrains.Annotations;
using Serilog;
using Serilog.Core;
using Trash.Command.Helpers;
using Trash.Config;
using TrashLib.Config.Settings;
using TrashLib.Sonarr.Config;
using TrashLib.Sonarr.QualityDefinition;
using TrashLib.Sonarr.ReleaseProfile;
@ -27,10 +27,11 @@ public class SonarrCommand : ServiceCommand
ILogger log,
LoggingLevelSwitch loggingLevelSwitch,
ILogJanitor logJanitor,
ISettingsPersister settingsPersister,
IConfigurationLoader<SonarrConfiguration> configLoader,
Func<IReleaseProfileUpdater> profileUpdaterFactory,
Func<ISonarrQualityDefinitionUpdater> qualityUpdaterFactory)
: base(log, loggingLevelSwitch, logJanitor)
: base(log, loggingLevelSwitch, logJanitor, settingsPersister)
{
_log = log;
_configLoader = configLoader;

@ -22,13 +22,13 @@ public class ConfigurationLoader<T> : IConfigurationLoader<T>
public ConfigurationLoader(
IConfigurationProvider configProvider,
IFileSystem fileSystem,
IYamlDeserializerFactory yamlFactory,
IYamlSerializerFactory yamlFactory,
IValidator<T> validator)
{
_configProvider = configProvider;
_fileSystem = fileSystem;
_validator = validator;
_deserializer = yamlFactory.Create();
_deserializer = yamlFactory.CreateDeserializer();
}
public IEnumerable<T> Load(string propertyName, string configSection)

@ -1,6 +1,5 @@
using System.Collections.Generic;
using System.IO;
using TrashLib.Config;
using TrashLib.Config.Services;
namespace Trash.Config;

@ -5,4 +5,5 @@ namespace Trash;
public class ResourcePaths : IResourcePaths
{
public string RepoPath => AppPaths.RepoDirectory;
public string SettingsPath => AppPaths.DefaultSettingsPath;
}

@ -10,7 +10,6 @@ using NUnit.Framework;
using Serilog;
using TestLibrary.NSubstitute;
using TrashLib.Cache;
using TrashLib.Config;
using TrashLib.Config.Services;
namespace TrashLib.Tests.Cache;

@ -0,0 +1,69 @@
using System.IO.Abstractions.TestingHelpers;
using AutoFixture.NUnit3;
using FluentAssertions;
using NSubstitute;
using NUnit.Framework;
using TestLibrary.AutoFixture;
using TrashLib.Config;
using TrashLib.Config.Settings;
using TrashLib.Radarr.Config;
using YamlDotNet.Serialization;
namespace TrashLib.Tests.Config.Settings;
[TestFixture]
[Parallelizable(ParallelScope.All)]
public class SettingsPersisterTest
{
[Test, AutoMockData]
public void Load_should_create_settings_file_if_not_exists(
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fileSystem,
[Frozen] IResourcePaths paths,
SettingsPersister sut)
{
paths.SettingsPath.Returns("test_path");
sut.Load();
fileSystem.AllFiles.Should().ContainSingle(x => x.EndsWith(paths.SettingsPath));
}
[Test, AutoMockData]
public void Load_defaults_when_file_does_not_exist(
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fileSystem,
[Frozen(Matching.ImplementedInterfaces)] YamlSerializerFactory serializerFactory,
[Frozen(Matching.ImplementedInterfaces)] SettingsProvider settingsProvider,
[Frozen] IResourcePaths paths,
SettingsPersister sut)
{
paths.SettingsPath.Returns("test_path");
sut.Load();
var expectedSettings = new SettingsValues();
settingsProvider.Settings.Should().BeEquivalentTo(expectedSettings);
}
[Test, AutoMockData]
public void Load_data_correctly_when_file_exists(
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fileSystem,
[Frozen] IYamlSerializerFactory serializerFactory,
[Frozen] IResourcePaths paths,
SettingsPersister sut)
{
// For this test, it doesn't really matter if the YAML data matches what SettingsValue expects;
// this test only ensures that the data deserialized is from the actual correct file.
var expectedYamlData = @"
repository:
clone_url: http://the_url.com
";
var deserializer = Substitute.For<IDeserializer>();
serializerFactory.CreateDeserializer().Returns(deserializer);
paths.SettingsPath.Returns("test_path");
fileSystem.AddFile(paths.SettingsPath, new MockFileData(expectedYamlData));
sut.Load();
deserializer.Received().Deserialize<SettingsValues>(expectedYamlData);
}
}

@ -0,0 +1,19 @@
using FluentAssertions;
using NUnit.Framework;
using TestLibrary.AutoFixture;
using TrashLib.Config.Settings;
namespace TrashLib.Tests.Config.Settings;
[TestFixture]
[Parallelizable(ParallelScope.All)]
public class SettingsProviderTest
{
[Test, AutoMockData]
public void Property_returns_same_value_from_set_method(SettingsProvider sut)
{
var settings = new SettingsValues();
sut.UseSettings(settings);
sut.Settings.Should().Be(settings);
}
}

@ -4,7 +4,6 @@ using System.Collections.ObjectModel;
using Newtonsoft.Json.Linq;
using NSubstitute;
using NUnit.Framework;
using TrashLib.Config;
using TrashLib.Config.Services;
using TrashLib.Radarr.Config;
using TrashLib.Radarr.CustomFormat.Api;

@ -9,7 +9,6 @@ using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using Serilog;
using TrashLib.Config;
using TrashLib.Config.Services;
namespace TrashLib.Cache;

@ -2,6 +2,7 @@ using System.Reflection;
using Autofac;
using FluentValidation;
using TrashLib.Config.Services;
using TrashLib.Config.Settings;
using Module = Autofac.Module;
namespace TrashLib.Config;
@ -10,14 +11,13 @@ public class ConfigAutofacModule : Module
{
protected override void Load(ContainerBuilder builder)
{
builder.RegisterType<ConfigurationProvider>()
.As<IConfigurationProvider>()
.SingleInstance();
builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly())
.AsClosedTypesOf(typeof(IValidator<>))
.AsImplementedInterfaces();
builder.RegisterType<YamlDeserializerFactory>().As<IYamlDeserializerFactory>();
builder.RegisterType<ConfigurationProvider>().As<IConfigurationProvider>().SingleInstance();
builder.RegisterType<SettingsProvider>().As<ISettingsProvider>().SingleInstance();
builder.RegisterType<YamlSerializerFactory>().As<IYamlSerializerFactory>();
builder.RegisterType<SettingsPersister>().As<ISettingsPersister>();
}
}

@ -0,0 +1,6 @@
namespace TrashLib.Config.Settings;
public interface ISettingsPersister
{
void Load();
}

@ -0,0 +1,7 @@
namespace TrashLib.Config.Settings;
public interface ISettingsProvider
{
SettingsValues Settings { get; }
void UseSettings(SettingsValues settings);
}

@ -0,0 +1,51 @@
using System.IO.Abstractions;
using TrashLib.Radarr.Config;
namespace TrashLib.Config.Settings;
public class SettingsPersister : ISettingsPersister
{
private readonly IResourcePaths _paths;
private readonly ISettingsProvider _settingsProvider;
private readonly IYamlSerializerFactory _serializerFactory;
private readonly IFileSystem _fileSystem;
public SettingsPersister(
IResourcePaths paths,
ISettingsProvider settingsProvider,
IYamlSerializerFactory serializerFactory,
IFileSystem fileSystem)
{
_paths = paths;
_settingsProvider = settingsProvider;
_serializerFactory = serializerFactory;
_fileSystem = fileSystem;
}
public void Load()
{
var deserializer = _serializerFactory.CreateDeserializer();
var settings = deserializer.Deserialize<SettingsValues?>(LoadOrCreateSettingsFile()) ?? new SettingsValues();
_settingsProvider.UseSettings(settings);
}
private string LoadOrCreateSettingsFile()
{
if (!_fileSystem.File.Exists(_paths.SettingsPath))
{
CreateDefaultSettingsFile();
}
return _fileSystem.File.ReadAllText(_paths.SettingsPath);
}
private void CreateDefaultSettingsFile()
{
const string fileData =
"# Edit this file to customize the behavior of Trash Updater beyond its defaults\n" +
"# For the settings file reference guide, visit the link to the wiki below:\n" +
"# https://github.com/rcdailey/trash-updater/wiki/Settings-Reference\n";
_fileSystem.File.WriteAllText(_paths.SettingsPath, fileData);
}
}

@ -0,0 +1,11 @@
namespace TrashLib.Config.Settings;
public class SettingsProvider : ISettingsProvider
{
public SettingsValues Settings { get; private set; } = new();
public void UseSettings(SettingsValues settings)
{
Settings = settings;
}
}

@ -0,0 +1,11 @@
namespace TrashLib.Config.Settings;
public record TrashRepository
{
public string CloneUrl { get; init; } = "https://github.com/TRaSH-/Guides.git";
}
public record SettingsValues
{
public TrashRepository Repository { get; init; } = new();
}

@ -3,4 +3,5 @@ namespace TrashLib.Radarr.Config;
public interface IResourcePaths
{
string RepoPath { get; }
string SettingsPath { get; }
}

@ -1,6 +1,5 @@
using System.Collections.Generic;
using JetBrains.Annotations;
using TrashLib.Config;
using TrashLib.Config.Services;
using TrashLib.Radarr.QualityDefinition;

@ -2,7 +2,6 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using Flurl.Http;
using Newtonsoft.Json.Linq;
using TrashLib.Config;
using TrashLib.Config.Services;
using TrashLib.Radarr.CustomFormat.Models;

@ -2,7 +2,6 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using Flurl.Http;
using Newtonsoft.Json.Linq;
using TrashLib.Config;
using TrashLib.Config.Services;
namespace TrashLib.Radarr.CustomFormat.Api;

@ -4,6 +4,8 @@ using System.IO.Abstractions;
using System.Linq;
using System.Threading.Tasks;
using LibGit2Sharp;
using Serilog;
using TrashLib.Config.Settings;
using TrashLib.Radarr.Config;
namespace TrashLib.Radarr.CustomFormat.Guide
@ -11,48 +13,70 @@ namespace TrashLib.Radarr.CustomFormat.Guide
internal class LocalRepoCustomFormatJsonParser : IRadarrGuideService
{
private readonly IFileSystem _fileSystem;
private readonly ISettingsProvider _settings;
private readonly ILogger _log;
private readonly string _repoPath;
public LocalRepoCustomFormatJsonParser(IFileSystem fileSystem, IResourcePaths paths)
public LocalRepoCustomFormatJsonParser(
IFileSystem fileSystem,
IResourcePaths paths,
ISettingsProvider settings,
ILogger log)
{
_fileSystem = fileSystem;
_settings = settings;
_log = log;
_repoPath = paths.RepoPath;
}
public async Task<IEnumerable<string>> GetCustomFormatJsonAsync()
{
await Task.Run(() =>
CloneOrUpdateGitRepo();
var jsonDir = Path.Combine(_repoPath, "docs/json/radarr");
var tasks = _fileSystem.Directory.GetFiles(jsonDir, "*.json")
.Select(async f => await _fileSystem.File.ReadAllTextAsync(f));
return await Task.WhenAll(tasks);
}
private void CloneOrUpdateGitRepo()
{
var cloneUrl = _settings.Settings.Repository.CloneUrl;
if (!Repository.IsValid(_repoPath))
{
if (!Repository.IsValid(_repoPath))
if (_fileSystem.Directory.Exists(_repoPath))
{
if (_fileSystem.Directory.Exists(_repoPath))
{
_fileSystem.Directory.Delete(_repoPath, true);
}
Repository.Clone("https://github.com/TRaSH-/Guides.git", _repoPath, new CloneOptions
{
RecurseSubmodules = false
});
_fileSystem.Directory.Delete(_repoPath, true);
}
using var repo = new Repository(_repoPath);
Commands.Checkout(repo, "master", new CheckoutOptions
Repository.Clone(cloneUrl, _repoPath, new CloneOptions
{
CheckoutModifiers = CheckoutModifiers.Force
RecurseSubmodules = false
});
}
var origin = repo.Network.Remotes["origin"];
Commands.Fetch(repo, origin.Name, origin.FetchRefSpecs.Select(s => s.Specification), null, "");
repo.Reset(ResetMode.Hard, repo.Branches["origin/master"].Tip);
using var repo = new Repository(_repoPath);
Commands.Checkout(repo, "master", new CheckoutOptions
{
CheckoutModifiers = CheckoutModifiers.Force
});
var jsonDir = Path.Combine(_repoPath, "docs/json/radarr");
var tasks = _fileSystem.Directory.GetFiles(jsonDir, "*.json")
.Select(async f => await _fileSystem.File.ReadAllTextAsync(f));
var origin = repo.Network.Remotes["origin"];
if (origin.Url != cloneUrl)
{
_log.Debug(
"Origin's URL ({OriginUrl}) does not match the clone URL from settings ({CloneUrl}) and will be updated",
origin.Url, cloneUrl);
return await Task.WhenAll(tasks);
repo.Network.Remotes.Update("origin", updater => updater.Url = cloneUrl);
origin = repo.Network.Remotes["origin"];
}
Commands.Fetch(repo, origin.Name, origin.FetchRefSpecs.Select(s => s.Specification), null, "");
repo.Reset(ResetMode.Hard, repo.Branches["origin/master"].Tip);
}
}
}

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using TrashLib.Config;
using TrashLib.Config.Services;
using TrashLib.Radarr.Config;
using TrashLib.Radarr.CustomFormat.Api;

@ -1,7 +1,6 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Flurl.Http;
using TrashLib.Config;
using TrashLib.Config.Services;
using TrashLib.Radarr.QualityDefinition.Api.Objects;

@ -1,6 +1,5 @@
using Autofac;
using Autofac.Extras.AggregateService;
using TrashLib.Config;
using TrashLib.Config.Services;
using TrashLib.Radarr.Config;
using TrashLib.Radarr.CustomFormat;

@ -4,7 +4,6 @@ using System.Threading.Tasks;
using Flurl.Http;
using Newtonsoft.Json.Linq;
using Serilog;
using TrashLib.Config;
using TrashLib.Config.Services;
using TrashLib.Extensions;
using TrashLib.Sonarr.Api.Objects;

@ -1,5 +1,4 @@
using System.Collections.Generic;
using TrashLib.Config;
using TrashLib.Config.Services;
using TrashLib.Sonarr.QualityDefinition;
using TrashLib.Sonarr.ReleaseProfile;

@ -2,7 +2,6 @@ using System;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using Flurl.Http;
using TrashLib.Config;
using TrashLib.Config.Services;
namespace TrashLib.Sonarr;

@ -2,6 +2,7 @@ Pages of Interest:
- [[Command Line Reference]]
- [[Configuration Reference]]
- [[Settings Reference]]
- [[TRaSH Guide Structural Guidelines]]
See the "Pages" list on the right side of this page for the complete list of wiki pages.

@ -0,0 +1,36 @@
This page contains the YAML reference for `settings.yml`. This file is located in the following
locations depending on your platform:
| Platform | Location |
| -------- | ---------------------------------------------------------- |
| Windows | `%APPDATA%\trash-updater\settings.yml` |
| Linux | `~/.config/trash-updater/settings.yml` |
| MacOS | `~/Library/Application Support/trash-updater/settings.yml` |
Settings in this file affect the behavior of Trash Updater regardless of instance-specific
configuration for Radarr and Sonarr.
If this file does not exist, Trash Updater will create it for you. Starting out, this file will be
empty and default behavior will be used. There is absolutely no need to touch this file unless you
have a specific reason to. It is recommended that you only add the specific properties for the
customizations you need and leave the rest alone.
# YAML Reference
Table of Contents
- [Repository Settings](#repository-settings)
## Repository Settings
```yml
repository:
clone_url: https://github.com/TRaSH-/Guides.git
```
- `clone_url`<br>
A URL compatible with `git clone` that is used to clone the [Trash Guides
repository](official_repo). This setting exists for enthusiasts that may want to instead have
Trash Updater pull data from a fork instead of the official repository.
[official_repo]: https://github.com/TRaSH-/Guides
Loading…
Cancel
Save