fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Add initial Blazor Server project

recyclarr
Robert Dailey 3 years ago
parent 0ce95b58c0
commit 085d641e7e

@ -0,0 +1,10 @@
using Microsoft.EntityFrameworkCore;
using TrashLib.Radarr.Config;
namespace Recyclarr.Code.Database
{
public class DatabaseContext : DbContext
{
public DbSet<RadarrConfig> RadarrConfigs { get; set; }
}
}

@ -1,59 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using TrashLib.Radarr.CustomFormat.Guide;
namespace Recyclarr.Code.Radarr
{
public class CustomFormatIdentifier
{
public CustomFormatIdentifier(string name, string trashId)
{
Name = name;
TrashId = trashId;
}
public string Name { get; }
public string TrashId { get; }
}
public class CustomFormatRepository
{
private readonly IRadarrGuideService _guideService;
private List<CustomFormatIdentifier>? _customFormatIdentifiers;
public CustomFormatRepository(IRadarrGuideService guideService)
{
_guideService = guideService;
}
public IList<CustomFormatIdentifier> Identifiers =>
_customFormatIdentifiers ?? throw new NullReferenceException(
"CustomFormatRepository.BuildRepository() must be called before requesting a list of CF identifiers");
public bool IsLoaded => _customFormatIdentifiers != null;
public async Task ForceRebuildRepository()
{
_customFormatIdentifiers = null;
await BuildRepository();
}
public async Task<bool> BuildRepository()
{
if (_customFormatIdentifiers != null)
{
return false;
}
_customFormatIdentifiers = (await _guideService.GetCustomFormatJson())
.Select(JObject.Parse)
.Select(json => new CustomFormatIdentifier((string) json["name"], (string) json["trash_id"]))
.ToList();
return true;
}
}
}

@ -0,0 +1,103 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Serilog;
using TrashLib.Radarr.Config;
using TrashLib.Radarr.CustomFormat.Api;
using TrashLib.Radarr.CustomFormat.Cache;
using TrashLib.Radarr.CustomFormat.Guide;
using TrashLib.Radarr.CustomFormat.Models;
using TrashLib.Radarr.CustomFormat.Models.Cache;
using TrashLib.Radarr.CustomFormat.Processors.GuideSteps;
using TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps;
namespace Recyclarr.Code.Radarr
{
internal class GuideProcessor : IGuideProcessor
{
private readonly ICachePersisterFactory _cachePersisterFactory;
private readonly ICustomFormatApiPersistenceStep _customFormatCustomFormatApiPersister;
private readonly ICustomFormatProcessor _customFormatProcessor;
private readonly Func<string, ICustomFormatService> _customFormatServiceFactory;
private readonly IRadarrGuideService _guideService;
private readonly IJsonTransactionStep _jsonTransactionStep;
private IList<string>? _guideCustomFormatJson;
public GuideProcessor(
ILogger log,
IRadarrGuideService guideService,
ICustomFormatProcessor customFormatProcessor,
ICachePersisterFactory cachePersisterFactory,
Func<string, ICustomFormatService> customFormatServiceFactory,
IJsonTransactionStep jsonTransactionStep,
ICustomFormatApiPersistenceStep customFormatCustomFormatApiPersister)
{
Log = log;
_guideService = guideService;
_customFormatProcessor = customFormatProcessor;
_cachePersisterFactory = cachePersisterFactory;
_customFormatServiceFactory = customFormatServiceFactory;
_jsonTransactionStep = jsonTransactionStep;
_customFormatCustomFormatApiPersister = customFormatCustomFormatApiPersister;
}
private ILogger Log { get; }
public IReadOnlyCollection<ProcessedCustomFormatData> ProcessedCustomFormats
=> _customFormatProcessor.ProcessedCustomFormats;
public IReadOnlyCollection<TrashIdMapping> DeletedCustomFormatsInCache
=> _customFormatProcessor.DeletedCustomFormatsInCache;
public List<(string, string)> CustomFormatsWithOutdatedNames
=> _customFormatProcessor.CustomFormatsWithOutdatedNames;
public bool IsLoaded => _guideCustomFormatJson is not null;
public async Task ForceBuildGuideData(RadarrConfig config)
{
_guideCustomFormatJson = null;
await BuildGuideData(config);
}
public async Task BuildGuideData(RadarrConfig config)
{
var cache = _cachePersisterFactory.Create(config);
cache.Load();
if (_guideCustomFormatJson == null)
{
Log.Debug("Requesting and parsing guide markdown");
_guideCustomFormatJson = (await _guideService.GetCustomFormatJson()).ToList();
}
// Process and filter the custom formats from the guide.
// Custom formats in the guide not mentioned in the config are filtered out.
_customFormatProcessor.Process(_guideCustomFormatJson, config, cache.CfCache);
cache.Save();
}
public async Task SaveToRadarr(RadarrConfig config)
{
var customFormatService = _customFormatServiceFactory(config.BuildUrl());
var radarrCfs = await customFormatService.GetCustomFormats();
// Match CFs between the guide & Radarr and merge the data. The goal is to retain as much of the
// original data from Radarr as possible. There are many properties in the response JSON that we don't
// directly care about. We keep those and just update the ones we do care about.
_jsonTransactionStep.Process(ProcessedCustomFormats, radarrCfs);
// Step 1.1: Optionally record deletions of custom formats in cache but not in the guide
if (config.DeleteOldCustomFormats)
{
_jsonTransactionStep.RecordDeletions(DeletedCustomFormatsInCache, radarrCfs);
}
// Step 2: For each merged CF, persist it to Radarr via its API. This will involve a combination of updates
// to existing CFs and creation of brand new ones, depending on what's already available in Radarr.
await _customFormatCustomFormatApiPersister.Process(
customFormatService, _jsonTransactionStep.Transactions);
}
}
}

@ -0,0 +1,19 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using TrashLib.Radarr.Config;
using TrashLib.Radarr.CustomFormat.Models;
using TrashLib.Radarr.CustomFormat.Models.Cache;
namespace Recyclarr.Code.Radarr
{
public interface IGuideProcessor
{
IReadOnlyCollection<ProcessedCustomFormatData> ProcessedCustomFormats { get; }
IReadOnlyCollection<TrashIdMapping> DeletedCustomFormatsInCache { get; }
List<(string, string)> CustomFormatsWithOutdatedNames { get; }
bool IsLoaded { get; }
Task BuildGuideData(RadarrConfig config);
Task SaveToRadarr(RadarrConfig config);
Task ForceBuildGuideData(RadarrConfig config);
}
}

@ -34,14 +34,15 @@ namespace Recyclarr
{
builder.RegisterModule<RadarrAutofacModule>();
builder.RegisterType<GuideProcessor>().As<IGuideProcessor>();
builder.RegisterType<CustomFormatRepository>()
.InstancePerLifetimeScope();
builder.RegisterType<ConfigPersister<RadarrConfiguration>>()
.As<IConfigPersister<RadarrConfiguration>>()
builder.RegisterType<ConfigPersister<RadarrConfig>>()
.As<IConfigPersister<RadarrConfig>>()
.WithParameter(new NamedParameter("filename", "radarr.json"));
builder.Register(c => c.Resolve<IConfigPersister<RadarrConfiguration>>().Load())
builder.Register(c => c.Resolve<IConfigPersister<RadarrConfig>>().Load())
.InstancePerLifetimeScope();
}
}

@ -17,7 +17,7 @@
<MudButton Class="mud-theme-primary my-2" OnClick="@ForceReload">Retry</MudButton>
</MudContainer>
}
else if (!CfRepository.IsLoaded)
else if (!GuideProcessor.IsLoaded)
{
<MudContainer Class="d-flex flex-column align-center">
<MudText Align="Align.Center" Class="my-2">Loading custom formats...</MudText>

@ -12,9 +12,9 @@ namespace Recyclarr.Pages.Radarr.CustomFormats
public Action? OnReload { get; set; }
[Inject]
public CustomFormatRepository CfRepository { get; set; } = default!;
public IGuideProcessor GuideProcessor { get; set; } = default!;
public bool IsLoaded => CfRepository.IsLoaded;
public bool IsLoaded => GuideProcessor.IsLoaded;
protected override async Task OnInitializedAsync()
{
@ -32,11 +32,11 @@ namespace Recyclarr.Pages.Radarr.CustomFormats
if (force)
{
await CfRepository.ForceRebuildRepository();
await GuideProcessor.ForceBuildGuideData();
}
else
{
wasLoaded = await CfRepository.BuildRepository();
wasLoaded = await GuideProcessor.BuildRepository();
}
if (wasLoaded)

@ -17,7 +17,7 @@ namespace Recyclarr.Pages.Radarr.CustomFormats
{
private CustomFormatChooser? _cfChooser;
private HashSet<SelectableCustomFormat> _currentSelection = new();
private ServerSelector<RadarrConfiguration>? _serverSelector;
private ServerSelector<RadarrConfig>? _serverSelector;
[CascadingParameter]
public CustomFormatAccessLayout? CfAccessor { get; set; }
@ -26,10 +26,10 @@ namespace Recyclarr.Pages.Radarr.CustomFormats
public IDialogService DialogService { get; set; } = default!;
[Inject]
public IConfigPersister<RadarrConfiguration> ConfigPersister { get; set; } = default!;
public IConfigPersister<RadarrConfig> ConfigPersister { get; set; } = default!;
[Inject]
public ICollection<RadarrConfiguration> Configs { get; set; } = default!;
public ICollection<RadarrConfig> Configs { get; set; } = default!;
private bool? SelectAllCheckbox { get; set; } = false;
private List<string> ChosenCustomFormatIds => _currentSelection.Select(cf => cf.Item.TrashIds.First()).ToList();
@ -37,7 +37,7 @@ namespace Recyclarr.Pages.Radarr.CustomFormats
private bool IsAddSelectedDisabled => IsRefreshDisabled || _cfChooser!.SelectedCount == 0;
private IList<CustomFormatIdentifier> CustomFormatIds
=> CfAccessor?.CfRepository.Identifiers ?? new List<CustomFormatIdentifier>();
=> CfAccessor?.GuideProcessor.ProcessedCustomFormats ?? new List<CustomFormatIdentifier>();
public void Dispose()
{
@ -63,7 +63,7 @@ namespace Recyclarr.Pages.Radarr.CustomFormats
StateHasChanged();
}
private void UpdateSelectedCustomFormats(RadarrConfiguration? selection)
private void UpdateSelectedCustomFormats(RadarrConfig? selection)
{
if (selection == null)
{
@ -73,7 +73,7 @@ namespace Recyclarr.Pages.Radarr.CustomFormats
_currentSelection = selection.CustomFormats
.Select(cf =>
{
var exists = CfAccessor?.CfRepository.Identifiers.Any(id2 => id2.TrashId == cf.TrashIds.First());
var exists = CfAccessor?.GuideProcessor.Identifiers.Any(id2 => id2.TrashId == cf.TrashIds.First());
return new SelectableCustomFormat(cf, exists ?? false);
})
.ToHashSet();

@ -7,6 +7,7 @@
@using Flurl.Http
@using TrashLib.Cache
@using TrashLib.Radarr.CustomFormat
@using TrashLib.Radarr.CustomFormat.Cache
<div class="d-flex mb-4 flex-column flex-sm-row">
@ -105,10 +106,10 @@
public Func<string, IQualityProfileService> ProfileServiceFactory { get; set; } = default!;
[Inject]
public IConfigPersister<RadarrConfiguration> ConfigPersister { get; set; } = default!;
public IConfigPersister<RadarrConfig> ConfigPersister { get; set; } = default!;
[Inject]
public ICollection<RadarrConfiguration> Configs { get; set; } = default!;
public ICollection<RadarrConfig> Configs { get; set; } = default!;
protected override async Task OnInitializedAsync()
{
@ -117,25 +118,25 @@
class ProfileSelectionPageManager
{
private readonly Func<ICacheGuidBuilder, ICachePersister> _cachePersisterFactory;
private readonly ICachePersisterFactory _cachePersisterFactory;
private readonly Func<string, ICustomFormatService> _customFormatServiceFactory;
public ProfileSelectionPageManager(
Func<ICacheGuidBuilder, ICachePersister> cachePersisterFactory,
ICachePersisterFactory cachePersisterFactory,
Func<string, ICustomFormatService> customFormatServiceFactory)
{
_cachePersisterFactory = cachePersisterFactory;
_customFormatServiceFactory = customFormatServiceFactory;
}
async Task RequestCustomFormatsAndUpdateCache(RadarrConfiguration config)
async Task RequestCustomFormatsAndUpdateCache(RadarrConfig config)
{
var cfService = _customFormatServiceFactory(config.BuildUrl());
var customFormats = await cfService.GetCustomFormats();
}
}
private async Task OnSelectedInstanceChanged(RadarrConfiguration? activeConfig)
private async Task OnSelectedInstanceChanged(RadarrConfig? activeConfig)
{
try
{
@ -151,9 +152,9 @@
// - Need to pair guide score with current profile score
// - Exclude FormatItems that represent Custom Formats not selected by the user
//
var qualityProfiles = await ProfileServiceFactory(activeConfig.BuildUrl()).GetQualityProfiles();
qualityProfiles.Where(_activeConfig.CustomFormats)
_profiles.AddRange();
// var qualityProfiles = await ProfileServiceFactory(activeConfig.BuildUrl()).GetQualityProfiles();
// qualityProfiles.Where(_activeConfig.CustomFormats)
// _profiles.AddRange();
}
SelectedProfile = _profiles.FirstOrDefault();
@ -166,8 +167,8 @@
_loading = false;
}
private RadarrConfiguration? _activeConfig;
private ServerSelector<RadarrConfiguration>? _serverSelector;
private RadarrConfig? _activeConfig;
private ServerSelector<RadarrConfig>? _serverSelector;
private readonly List<QualityProfileData> _profiles = new();
private Exception? _exception;
private bool _loading;

@ -15,10 +15,10 @@ namespace Recyclarr.Pages.Radarr.Servers
public IDialogService DialogService { get; set; } = default!;
[Inject]
public IConfigPersister<RadarrConfiguration> ConfigPersister { get; set; } = default!;
public IConfigPersister<RadarrConfig> ConfigPersister { get; set; } = default!;
[Inject]
public ICollection<RadarrConfiguration> Configs { get; set; } = default!;
public ICollection<RadarrConfig> Configs { get; set; } = default!;
private async Task<bool> ShowEditServerModal(string title, ServiceConfiguration instance)
{
@ -47,7 +47,7 @@ namespace Recyclarr.Pages.Radarr.Servers
private async Task OnAddServer()
{
var item = new RadarrConfiguration();
var item = new RadarrConfig();
if (await ShowEditServerModal("Add Server", item))
{
Configs.Add(item);
@ -55,7 +55,7 @@ namespace Recyclarr.Pages.Radarr.Servers
}
}
private async Task OnEdit(RadarrConfiguration item)
private async Task OnEdit(RadarrConfig item)
{
await ShowEditServerModal("Edit Server", item);
SaveServers();
@ -66,7 +66,7 @@ namespace Recyclarr.Pages.Radarr.Servers
ConfigPersister.Save(Configs);
}
private async Task OnDelete(RadarrConfiguration item)
private async Task OnDelete(RadarrConfig item)
{
var shouldDelete = await DialogService.ShowMessageBox(
"Warning",

@ -6,6 +6,9 @@
<PackageReference Include="BlazorPro.BlazorSize" Version="5.*" />
<PackageReference Include="Blazored.LocalStorage" Version="4.*" />
<PackageReference Include="MudBlazor" Version="5.*" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.*" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.*" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.*" />
</ItemGroup>
<ItemGroup>

@ -18,7 +18,7 @@ namespace Trash.Command
[UsedImplicitly]
public class RadarrCommand : ServiceCommand
{
private readonly IConfigurationLoader<RadarrConfiguration> _configLoader;
private readonly IConfigurationLoader<RadarrConfig> _configLoader;
private readonly Func<ICustomFormatUpdater> _customFormatUpdaterFactory;
private readonly Func<IRadarrQualityDefinitionUpdater> _qualityUpdaterFactory;
@ -26,7 +26,7 @@ namespace Trash.Command
ILogger logger,
LoggingLevelSwitch loggingLevelSwitch,
ILogJanitor logJanitor,
IConfigurationLoader<RadarrConfiguration> configLoader,
IConfigurationLoader<RadarrConfig> configLoader,
Func<IRadarrQualityDefinitionUpdater> qualityUpdaterFactory,
Func<ICustomFormatUpdater> customFormatUpdaterFactory)
: base(logger, loggingLevelSwitch, logJanitor)

@ -41,7 +41,7 @@ namespace TrashLib.Tests.Radarr.CustomFormat.Processors
[Test]
public void Custom_formats_are_deleted_if_deletion_option_is_enabled_in_config()
{
var config = new RadarrConfiguration {DeleteOldCustomFormats = true};
var config = new RadarrConfig {DeleteOldCustomFormats = true};
var ctx = new Context();
ctx.Processor.PersistCustomFormats(config, ctx.GuideCfs, ctx.DeletedCfsInCache, ctx.ProfileScores);
@ -52,7 +52,7 @@ namespace TrashLib.Tests.Radarr.CustomFormat.Processors
[Test]
public void Custom_formats_are_not_deleted_if_deletion_option_is_disabled_in_config()
{
var config = new RadarrConfiguration {DeleteOldCustomFormats = false};
var config = new RadarrConfig {DeleteOldCustomFormats = false};
var ctx = new Context();
ctx.Processor.PersistCustomFormats(config, ctx.GuideCfs, ctx.DeletedCfsInCache, ctx.ProfileScores);
@ -65,10 +65,10 @@ namespace TrashLib.Tests.Radarr.CustomFormat.Processors
{
var ctx = new Context();
var config = new RadarrConfiguration {DeleteOldCustomFormats = false};
var config = new RadarrConfig {DeleteOldCustomFormats = false};
ctx.Processor.PersistCustomFormats(config, ctx.GuideCfs, ctx.DeletedCfsInCache, ctx.ProfileScores);
config = new RadarrConfiguration {DeleteOldCustomFormats = true};
config = new RadarrConfig {DeleteOldCustomFormats = true};
ctx.Processor.PersistCustomFormats(config, ctx.GuideCfs, ctx.DeletedCfsInCache, ctx.ProfileScores);
ctx.Steps.JsonTransactionStep.Received(1)

@ -36,7 +36,7 @@ namespace TrashLib.Tests.Radarr
public void Custom_format_is_valid_with_one_of_either_names_or_trash_id(List<string> namesList,
List<string> trashIdsList)
{
var config = new RadarrConfiguration
var config = new RadarrConfig
{
ApiKey = "required value",
BaseUrl = "required value",
@ -46,7 +46,7 @@ namespace TrashLib.Tests.Radarr
}
};
var validator = _container.Resolve<IValidator<RadarrConfiguration>>();
var validator = _container.Resolve<IValidator<RadarrConfig>>();
var result = validator.Validate(config);
result.IsValid.Should().BeTrue();
@ -57,8 +57,8 @@ namespace TrashLib.Tests.Radarr
public void Validation_fails_for_all_missing_required_properties()
{
// default construct which should yield default values (invalid) for all required properties
var config = new RadarrConfiguration();
var validator = _container.Resolve<IValidator<RadarrConfiguration>>();
var config = new RadarrConfig();
var validator = _container.Resolve<IValidator<RadarrConfig>>();
var result = validator.Validate(config);
@ -79,7 +79,7 @@ namespace TrashLib.Tests.Radarr
[Test]
public void Validation_succeeds_when_no_missing_required_properties()
{
var config = new RadarrConfiguration
var config = new RadarrConfig
{
ApiKey = "required value",
BaseUrl = "required value",
@ -100,7 +100,7 @@ namespace TrashLib.Tests.Radarr
}
};
var validator = _container.Resolve<IValidator<RadarrConfiguration>>();
var validator = _container.Resolve<IValidator<RadarrConfig>>();
var result = validator.Validate(config);
result.IsValid.Should().BeTrue();

@ -1,40 +1,43 @@
using System.Collections.Generic;
using JetBrains.Annotations;
using TrashLib.Config;
using TrashLib.Radarr.QualityDefinition;
// ReSharper disable ClassNeverInstantiated.Global
namespace TrashLib.Radarr.Config
{
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
public class RadarrConfiguration : ServiceConfiguration
public class RadarrConfig : ServiceConfiguration
{
public int Id { get; set; }
public QualityDefinitionConfig? QualityDefinition { get; init; }
public List<CustomFormatConfig> CustomFormats { get; set; } = new();
public ICollection<CustomFormatConfig> CustomFormats { get; set; }// = new();
public ICollection<QualityProfileConfig> QualityProfiles { get; init; }// = new();
public bool DeleteOldCustomFormats { get; init; }
}
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
public class CustomFormatConfig
{
public List<string> Names { get; init; } = new();
public List<string> TrashIds { get; init; } = new();
public List<QualityProfileConfig> QualityProfiles { get; init; } = new();
public string Name { get; init; } = "";
public string TrashId { get; init; } = "";
}
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
public class QualityProfileConfig
{
public string Name { get; init; } = "";
public int? Score { get; init; }
public ICollection<ScoreEntryConfig> Scores { get; init; }// = new();
public bool ResetUnmatchedScores { get; init; }
}
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
public class ScoreEntryConfig
{
public string TrashId { get; init; } = "";
public int? Score { get; init; }
}
public class QualityDefinitionConfig
{
// -1 does not map to a valid enumerator. this is to force validation to fail if it is not set from YAML.
// All of this craziness is to avoid making the enum type nullable.
public RadarrQualityDefinitionType Type { get; init; } = (RadarrQualityDefinitionType) (-1);
public RadarrQualityDefinitionType Type { get; set; } = (RadarrQualityDefinitionType) (-1);
public decimal PreferredRatio { get; set; } = 1.0m;
}

@ -5,7 +5,7 @@ using JetBrains.Annotations;
namespace TrashLib.Radarr.Config
{
[UsedImplicitly]
internal class RadarrConfigurationValidator : AbstractValidator<RadarrConfiguration>
internal class RadarrConfigurationValidator : AbstractValidator<RadarrConfig>
{
public RadarrConfigurationValidator(
IRadarrValidationMessages messages,

@ -5,7 +5,7 @@ using TrashLib.Cache;
using TrashLib.Radarr.CustomFormat.Models;
using TrashLib.Radarr.CustomFormat.Models.Cache;
namespace TrashLib.Radarr.CustomFormat
namespace TrashLib.Radarr.CustomFormat.Cache
{
internal class CachePersister : ICachePersister
{
@ -20,28 +20,27 @@ namespace TrashLib.Radarr.CustomFormat
}
private ILogger Log { get; }
public CustomFormatCache? CfCache { get; private set; }
public void Load()
{
CfCache = _cache.Load<CustomFormatCache>(_guidBuilder);
// ReSharper disable once ConvertIfStatementToConditionalTernaryExpression
if (CfCache != null)
if (CfCache == null)
{
Log.Debug("Loaded Cache");
// If the version is higher OR lower, we invalidate the cache. It means there's an
// incompatibility that we do not support.
if (CfCache.Version != CustomFormatCache.LatestVersion)
{
Log.Information("Cache version mismatch ({OldVersion} vs {LatestVersion}); ignoring cache data",
CfCache.Version, CustomFormatCache.LatestVersion);
CfCache = null;
}
Log.Debug("Custom format cache does not exist; proceeding without it");
return;
}
else
Log.Debug("Loaded Cache");
// If the version is higher OR lower, we invalidate the cache. It means there's an
// incompatibility that we do not support.
if (CfCache.Version != CustomFormatCache.LatestVersion)
{
Log.Debug("Custom format cache does not exist; proceeding without it");
Log.Information("Cache version mismatch ({OldVersion} vs {LatestVersion}); ignoring cache data",
CfCache.Version, CustomFormatCache.LatestVersion);
CfCache = null;
}
}

@ -0,0 +1,25 @@
using System;
using TrashLib.Cache;
using TrashLib.Config;
namespace TrashLib.Radarr.CustomFormat.Cache
{
internal class CachePersisterFactory : ICachePersisterFactory
{
private readonly Func<IServiceConfiguration, ICacheGuidBuilder> _guidBuilderFactory;
private readonly Func<ICacheGuidBuilder, ICachePersister> _persisterFactory;
public CachePersisterFactory(
Func<IServiceConfiguration, ICacheGuidBuilder> guidBuilderFactory,
Func<ICacheGuidBuilder, ICachePersister> persisterFactory)
{
_guidBuilderFactory = guidBuilderFactory;
_persisterFactory = persisterFactory;
}
public ICachePersister Create(IServiceConfiguration config)
{
return _persisterFactory(_guidBuilderFactory(config));
}
}
}

@ -2,7 +2,7 @@ using System.Collections.Generic;
using TrashLib.Radarr.CustomFormat.Models;
using TrashLib.Radarr.CustomFormat.Models.Cache;
namespace TrashLib.Radarr.CustomFormat
namespace TrashLib.Radarr.CustomFormat.Cache
{
public interface ICachePersister
{

@ -0,0 +1,9 @@
using TrashLib.Config;
namespace TrashLib.Radarr.CustomFormat.Cache
{
public interface ICachePersisterFactory
{
ICachePersister Create(IServiceConfiguration config);
}
}

@ -4,6 +4,7 @@ using System.Threading.Tasks;
using Common.Extensions;
using Serilog;
using TrashLib.Radarr.Config;
using TrashLib.Radarr.CustomFormat.Cache;
using TrashLib.Radarr.CustomFormat.Processors;
using TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps;
@ -29,7 +30,7 @@ namespace TrashLib.Radarr.CustomFormat
private ILogger Log { get; }
public async Task Process(bool isPreview, RadarrConfiguration config)
public async Task Process(bool isPreview, RadarrConfig config)
{
_cache.Load();
@ -135,7 +136,7 @@ namespace TrashLib.Radarr.CustomFormat
}
}
private bool ValidateGuideDataAndCheckShouldProceed(RadarrConfiguration config)
private bool ValidateGuideDataAndCheckShouldProceed(RadarrConfig config)
{
Console.WriteLine("");

@ -5,6 +5,6 @@ namespace TrashLib.Radarr.CustomFormat
{
public interface ICustomFormatUpdater
{
Task Process(bool isPreview, RadarrConfiguration config);
Task Process(bool isPreview, RadarrConfig config);
}
}

@ -14,15 +14,13 @@ namespace TrashLib.Radarr.CustomFormat.Models.Cache
public class TrashIdMapping
{
public TrashIdMapping(string trashId, string customFormatName, int customFormatId = default)
public TrashIdMapping(string trashId, int customFormatId)
{
CustomFormatName = customFormatName;
TrashId = trashId;
CustomFormatId = customFormatId;
}
public string CustomFormatName { get; set; }
public string TrashId { get; }
public int CustomFormatId { get; set; }
public int CustomFormatId { get; }
}
}

@ -20,12 +20,9 @@ namespace TrashLib.Radarr.CustomFormat.Models
public JObject Json { get; set; }
public TrashIdMapping? CacheEntry { get; set; }
public string CacheAwareName => CacheEntry?.CustomFormatName ?? Name;
public void SetCache(int customFormatId)
{
CacheEntry ??= new TrashIdMapping(TrashId, Name);
CacheEntry.CustomFormatId = customFormatId;
CacheEntry = new TrashIdMapping(TrashId, customFormatId);
}
[SuppressMessage("Microsoft.Design", "CA1024", Justification = "Method throws an exception")]

@ -1,94 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Serilog;
using TrashLib.Radarr.Config;
using TrashLib.Radarr.CustomFormat.Guide;
using TrashLib.Radarr.CustomFormat.Models;
using TrashLib.Radarr.CustomFormat.Models.Cache;
using TrashLib.Radarr.CustomFormat.Processors.GuideSteps;
namespace TrashLib.Radarr.CustomFormat.Processors
{
public interface IGuideProcessorSteps
{
ICustomFormatStep CustomFormat { get; }
IConfigStep Config { get; }
IQualityProfileStep QualityProfile { get; }
}
internal class GuideProcessor : IGuideProcessor
{
private readonly IRadarrGuideService _guideService;
private readonly Func<IGuideProcessorSteps> _stepsFactory;
private IList<string>? _guideCustomFormatJson;
private IGuideProcessorSteps _steps;
public GuideProcessor(ILogger log, IRadarrGuideService guideService, Func<IGuideProcessorSteps> stepsFactory)
{
_guideService = guideService;
_stepsFactory = stepsFactory;
Log = log;
_steps = stepsFactory();
}
private ILogger Log { get; }
public IReadOnlyCollection<ProcessedCustomFormatData> ProcessedCustomFormats
=> _steps.CustomFormat.ProcessedCustomFormats;
public IReadOnlyCollection<string> CustomFormatsNotInGuide
=> _steps.Config.CustomFormatsNotInGuide;
public IReadOnlyCollection<ProcessedConfigData> ConfigData
=> _steps.Config.ConfigData;
public IDictionary<string, QualityProfileCustomFormatScoreMapping> ProfileScores
=> _steps.QualityProfile.ProfileScores;
public IReadOnlyCollection<(string name, string trashId, string profileName)> CustomFormatsWithoutScore
=> _steps.QualityProfile.CustomFormatsWithoutScore;
public IReadOnlyCollection<TrashIdMapping> DeletedCustomFormatsInCache
=> _steps.CustomFormat.DeletedCustomFormatsInCache;
public List<(string, string)> CustomFormatsWithOutdatedNames
=> _steps.CustomFormat.CustomFormatsWithOutdatedNames;
public Dictionary<string, List<ProcessedCustomFormatData>> DuplicatedCustomFormats
=> _steps.CustomFormat.DuplicatedCustomFormats;
public async Task BuildGuideData(IReadOnlyList<CustomFormatConfig> config, CustomFormatCache? cache)
{
if (_guideCustomFormatJson == null)
{
Log.Debug("Requesting and parsing guide markdown");
_guideCustomFormatJson = (await _guideService.GetCustomFormatJson()).ToList();
}
// Step 1: Process and filter the custom formats from the guide.
// Custom formats in the guide not mentioned in the config are filtered out.
_steps.CustomFormat.Process(_guideCustomFormatJson, config, cache);
// todo: Process cache entries that do not exist in the guide. Those should be deleted
// This might get taken care of when we rebuild the cache based on what is actually updated when
// we call the Radarr API
// Step 2: Use the processed custom formats from step 1 to process the configuration.
// CFs in config not in the guide are filtered out.
// Actual CF objects are associated to the quality profile objects to reduce lookups
_steps.Config.Process(_steps.CustomFormat.ProcessedCustomFormats, config);
// Step 3: Use the processed config (which contains processed CFs) to process the quality profile scores.
// Score precedence logic is utilized here to decide the CF score per profile (same CF can actually have
// different scores depending on which profile it goes into).
_steps.QualityProfile.Process(_steps.Config.ConfigData);
}
public void Reset()
{
_steps = _stepsFactory();
}
}
}

@ -1,68 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Common.Extensions;
using MoreLinq.Extensions;
using TrashLib.Radarr.Config;
using TrashLib.Radarr.CustomFormat.Models;
namespace TrashLib.Radarr.CustomFormat.Processors.GuideSteps
{
internal class ConfigStep : IConfigStep
{
public List<string> CustomFormatsNotInGuide { get; } = new();
public List<ProcessedConfigData> ConfigData { get; } = new();
public void Process(IReadOnlyCollection<ProcessedCustomFormatData> processedCfs,
IEnumerable<CustomFormatConfig> config)
{
foreach (var singleConfig in config)
{
var validCfs = new List<ProcessedCustomFormatData>();
foreach (var name in singleConfig.Names)
{
var match = FindCustomFormatByName(processedCfs, name);
if (match == null)
{
CustomFormatsNotInGuide.Add(name);
}
else
{
validCfs.Add(match);
}
}
foreach (var trashId in singleConfig.TrashIds)
{
var match = processedCfs.FirstOrDefault(cf => cf.TrashId.EqualsIgnoreCase(trashId));
if (match == null)
{
CustomFormatsNotInGuide.Add(trashId);
}
else
{
validCfs.Add(match);
}
}
ConfigData.Add(new ProcessedConfigData
{
QualityProfiles = singleConfig.QualityProfiles,
CustomFormats = validCfs
.DistinctBy(cf => cf.TrashId, StringComparer.InvariantCultureIgnoreCase)
.ToList()
});
}
}
private static ProcessedCustomFormatData? FindCustomFormatByName(
IReadOnlyCollection<ProcessedCustomFormatData> processedCfs, string name)
{
return processedCfs.FirstOrDefault(
cf => cf.CacheEntry?.CustomFormatName.EqualsIgnoreCase(name) ?? false) ??
processedCfs.FirstOrDefault(
cf => cf.Name.EqualsIgnoreCase(name));
}
}
}

@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json.Linq;
using TrashLib.Radarr.Config;
using TrashLib.Radarr.CustomFormat.Models;
using TrashLib.Radarr.CustomFormat.Models.Cache;
namespace TrashLib.Radarr.CustomFormat.Processors.GuideSteps
{
internal class CustomFormatProcessor : ICustomFormatProcessor
{
public List<(string, string)> CustomFormatsWithOutdatedNames { get; } = new();
public List<ProcessedCustomFormatData> ProcessedCustomFormats { get; } = new();
public List<TrashIdMapping> DeletedCustomFormatsInCache { get; } = new();
public void Process(IEnumerable<string> customFormatGuideData, RadarrConfig config, CustomFormatCache? cache)
{
var processedCfs = customFormatGuideData
.Select(jsonData => ProcessCustomFormatData(jsonData, cache))
.ToList();
// For each ID listed under the `trash_ids` YML property, match it to an existing CF
ProcessedCustomFormats.AddRange(config.CustomFormats
.Select(c => c.TrashId)
.Distinct()
.Join(processedCfs,
id => id,
cf => cf.TrashId,
(_, cf) => cf,
StringComparer.InvariantCultureIgnoreCase));
// Orphaned entries in cache represent custom formats we need to delete.
ProcessDeletedCustomFormats(cache);
}
private static ProcessedCustomFormatData ProcessCustomFormatData(string guideData, CustomFormatCache? cache)
{
JObject obj = JObject.Parse(guideData);
var name = (string) obj["name"];
var trashId = (string) obj["trash_id"];
int? finalScore = null;
if (obj.TryGetValue("trash_score", out var score))
{
finalScore = (int) score;
obj.Property("trash_score").Remove();
}
// Remove trash_id, it's metadata that is not meant for Radarr itself
// Radarr supposedly drops this anyway, but I prefer it to be removed by TrashUpdater
obj.Property("trash_id").Remove();
return new ProcessedCustomFormatData(name, trashId, obj)
{
Score = finalScore,
CacheEntry = cache?.TrashIdMappings.FirstOrDefault(c => c.TrashId == trashId)
};
}
private void ProcessDeletedCustomFormats(CustomFormatCache? cache)
{
if (cache == null)
{
return;
}
static bool MatchCfInCache(ProcessedCustomFormatData cf, TrashIdMapping c)
=> cf.CacheEntry != null && cf.CacheEntry.TrashId == c.TrashId;
// Delete if CF is in cache and not in the guide or config
DeletedCustomFormatsInCache.AddRange(cache.TrashIdMappings
.Where(c => !ProcessedCustomFormats.Any(cf => MatchCfInCache(cf, c))));
}
}
}

@ -1,135 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Common.Extensions;
using Newtonsoft.Json.Linq;
using TrashLib.Radarr.Config;
using TrashLib.Radarr.CustomFormat.Models;
using TrashLib.Radarr.CustomFormat.Models.Cache;
namespace TrashLib.Radarr.CustomFormat.Processors.GuideSteps
{
internal class CustomFormatStep : ICustomFormatStep
{
public List<(string, string)> CustomFormatsWithOutdatedNames { get; } = new();
public List<ProcessedCustomFormatData> ProcessedCustomFormats { get; } = new();
public List<TrashIdMapping> DeletedCustomFormatsInCache { get; } = new();
public Dictionary<string, List<ProcessedCustomFormatData>> DuplicatedCustomFormats { get; private set; } =
new();
public void Process(IEnumerable<string> customFormatGuideData,
IReadOnlyCollection<CustomFormatConfig> config, CustomFormatCache? cache)
{
var processedCfs = customFormatGuideData
.Select(cf => ProcessCustomFormatData(cf, cache))
.ToList();
// For each ID listed under the `trash_ids` YML property, match it to an existing CF
ProcessedCustomFormats.AddRange(config
.SelectMany(c => c.TrashIds)
.Distinct(StringComparer.CurrentCultureIgnoreCase)
.Join(processedCfs,
id => id,
cf => cf.TrashId,
(_, cf) => cf,
StringComparer.InvariantCultureIgnoreCase));
// Build a list of CF names under the `names` property in YAML. Exclude any names that
// are already provided by the `trash_ids` property.
var allConfigCfNames = config
.SelectMany(c => c.Names)
.Distinct(StringComparer.CurrentCultureIgnoreCase)
.Where(n => !ProcessedCustomFormats.Any(cf => cf.CacheAwareName.EqualsIgnoreCase(n)))
.ToList();
// Perform updates and deletions based on matches in the cache. Matches in the cache are by ID.
foreach (var cf in processedCfs)
{
// Does the name of the CF in the guide match a name in the config? If yes, we keep it.
var configName = allConfigCfNames.FirstOrDefault(n => n.EqualsIgnoreCase(cf.Name));
if (configName != null)
{
if (cf.CacheEntry != null)
{
// The cache entry might be using an old name. This will happen if:
// - A user has synced this CF before, AND
// - The name of the CF in the guide changed, AND
// - The user updated the name in their config to match the name in the guide.
cf.CacheEntry.CustomFormatName = cf.Name;
}
ProcessedCustomFormats.Add(cf);
continue;
}
// Does the name of the CF in the cache match a name in the config? If yes, we keep it.
configName = allConfigCfNames.FirstOrDefault(n => n.EqualsIgnoreCase(cf.CacheEntry?.CustomFormatName));
if (configName != null)
{
// Config name is out of sync with the guide and should be updated
CustomFormatsWithOutdatedNames.Add((configName, cf.Name));
ProcessedCustomFormats.Add(cf);
}
// If we get here, we can't find a match in the config using cache or guide name, so the user must have
// removed it from their config. This will get marked for deletion later.
}
// Orphaned entries in cache represent custom formats we need to delete.
ProcessDeletedCustomFormats(cache);
// Check for multiple custom formats with the same name in the guide data (e.g. "DoVi")
ProcessDuplicates();
}
private void ProcessDuplicates()
{
DuplicatedCustomFormats = ProcessedCustomFormats
.GroupBy(cf => cf.Name)
.Where(grp => grp.Count() > 1)
.ToDictionary(grp => grp.Key, grp => grp.ToList());
ProcessedCustomFormats.RemoveAll(cf => DuplicatedCustomFormats.ContainsKey(cf.Name));
}
private static ProcessedCustomFormatData ProcessCustomFormatData(string guideData, CustomFormatCache? cache)
{
JObject obj = JObject.Parse(guideData);
var name = (string) obj["name"];
var trashId = (string) obj["trash_id"];
int? finalScore = null;
if (obj.TryGetValue("trash_score", out var score))
{
finalScore = (int) score;
obj.Property("trash_score").Remove();
}
// Remove trash_id, it's metadata that is not meant for Radarr itself
// Radarr supposedly drops this anyway, but I prefer it to be removed by TrashUpdater
obj.Property("trash_id").Remove();
return new ProcessedCustomFormatData(name, trashId, obj)
{
Score = finalScore,
CacheEntry = cache?.TrashIdMappings.FirstOrDefault(c => c.TrashId == trashId)
};
}
private void ProcessDeletedCustomFormats(CustomFormatCache? cache)
{
if (cache == null)
{
return;
}
static bool MatchCfInCache(ProcessedCustomFormatData cf, TrashIdMapping c)
=> cf.CacheEntry != null && cf.CacheEntry.TrashId == c.TrashId;
// Delete if CF is in cache and not in the guide or config
DeletedCustomFormatsInCache.AddRange(cache.TrashIdMappings
.Where(c => !ProcessedCustomFormats.Any(cf => MatchCfInCache(cf, c))));
}
}
}

@ -1,15 +0,0 @@
using System.Collections.Generic;
using TrashLib.Radarr.Config;
using TrashLib.Radarr.CustomFormat.Models;
namespace TrashLib.Radarr.CustomFormat.Processors.GuideSteps
{
public interface IConfigStep
{
List<string> CustomFormatsNotInGuide { get; }
List<ProcessedConfigData> ConfigData { get; }
void Process(IReadOnlyCollection<ProcessedCustomFormatData> processedCfs,
IEnumerable<CustomFormatConfig> config);
}
}

@ -5,14 +5,12 @@ using TrashLib.Radarr.CustomFormat.Models.Cache;
namespace TrashLib.Radarr.CustomFormat.Processors.GuideSteps
{
public interface ICustomFormatStep
public interface ICustomFormatProcessor
{
List<(string, string)> CustomFormatsWithOutdatedNames { get; }
List<ProcessedCustomFormatData> ProcessedCustomFormats { get; }
List<TrashIdMapping> DeletedCustomFormatsInCache { get; }
List<(string, string)> CustomFormatsWithOutdatedNames { get; }
Dictionary<string, List<ProcessedCustomFormatData>> DuplicatedCustomFormats { get; }
void Process(IEnumerable<string> customFormatGuideData,
IReadOnlyCollection<CustomFormatConfig> config, CustomFormatCache? cache);
void Process(IEnumerable<string> customFormatGuideData, RadarrConfig config, CustomFormatCache? cache);
}
}

@ -1,23 +0,0 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using TrashLib.Radarr.Config;
using TrashLib.Radarr.CustomFormat.Models;
using TrashLib.Radarr.CustomFormat.Models.Cache;
namespace TrashLib.Radarr.CustomFormat.Processors
{
internal interface IGuideProcessor
{
IReadOnlyCollection<ProcessedCustomFormatData> ProcessedCustomFormats { get; }
IReadOnlyCollection<string> CustomFormatsNotInGuide { get; }
IReadOnlyCollection<ProcessedConfigData> ConfigData { get; }
IDictionary<string, QualityProfileCustomFormatScoreMapping> ProfileScores { get; }
IReadOnlyCollection<(string name, string trashId, string profileName)> CustomFormatsWithoutScore { get; }
IReadOnlyCollection<TrashIdMapping> DeletedCustomFormatsInCache { get; }
List<(string, string)> CustomFormatsWithOutdatedNames { get; }
Dictionary<string, List<ProcessedCustomFormatData>> DuplicatedCustomFormats { get; }
Task BuildGuideData(IReadOnlyList<CustomFormatConfig> config, CustomFormatCache? cache);
void Reset();
}
}

@ -14,7 +14,7 @@ namespace TrashLib.Radarr.CustomFormat.Processors
CustomFormatTransactionData Transactions { get; }
Task PersistCustomFormats(
RadarrConfiguration config,
RadarrConfig config,
IEnumerable<ProcessedCustomFormatData> guideCfs,
IEnumerable<TrashIdMapping> deletedCfsInCache,
IDictionary<string, QualityProfileCustomFormatScoreMapping> profileScores);

@ -49,7 +49,7 @@ namespace TrashLib.Radarr.CustomFormat.Processors
}
public async Task PersistCustomFormats(
RadarrConfiguration config,
RadarrConfig config,
IEnumerable<ProcessedCustomFormatData> guideCfs,
IEnumerable<TrashIdMapping> deletedCfsInCache,
IDictionary<string, QualityProfileCustomFormatScoreMapping> profileScores)

@ -5,6 +5,6 @@ namespace TrashLib.Radarr.QualityDefinition
{
public interface IRadarrQualityDefinitionUpdater
{
Task Process(bool isPreview, RadarrConfiguration config);
Task Process(bool isPreview, RadarrConfig config);
}
}

@ -24,7 +24,7 @@ namespace TrashLib.Radarr.QualityDefinition
private ILogger Log { get; }
public async Task Process(bool isPreview, RadarrConfiguration config)
public async Task Process(bool isPreview, RadarrConfig config)
{
Log.Information("Processing Quality Definition: {QualityDefinition}", config.QualityDefinition!.Type);
var qualityDefinitions = _parser.ParseMarkdown(await _parser.GetMarkdownData());

@ -1,11 +1,10 @@
using System;
using Autofac;
using Autofac.Extras.AggregateService;
using TrashLib.Cache;
using TrashLib.Config;
using TrashLib.Radarr.Config;
using TrashLib.Radarr.CustomFormat;
using TrashLib.Radarr.CustomFormat.Api;
using TrashLib.Radarr.CustomFormat.Cache;
using TrashLib.Radarr.CustomFormat.Guide;
using TrashLib.Radarr.CustomFormat.Processors;
using TrashLib.Radarr.CustomFormat.Processors.GuideSteps;
@ -17,25 +16,6 @@ namespace TrashLib.Radarr
{
public class RadarrAutofacModule : Module
{
// class CachePersisterFactory
// {
// private readonly Func<IServiceConfiguration, ICacheGuidBuilder> _guidBuilderFactory;
// private readonly Func<ICacheGuidBuilder, ICachePersister> _persisterFactory;
//
// public CachePersisterFactory(
// Func<IServiceConfiguration, ICacheGuidBuilder> guidBuilderFactory,
// Func<ICacheGuidBuilder, ICachePersister> persisterFactory)
// {
// _guidBuilderFactory = guidBuilderFactory;
// _persisterFactory = persisterFactory;
// }
//
// public ICachePersister Create(IServiceConfiguration config)
// {
// return _persisterFactory(_guidBuilderFactory(config));
// }
// }
protected override void Load(ContainerBuilder builder)
{
// Services
@ -54,20 +34,20 @@ namespace TrashLib.Radarr
// Custom Format Support
builder.RegisterType<CustomFormatUpdater>().As<ICustomFormatUpdater>();
builder.RegisterType<LocalRepoCustomFormatJsonParser>().As<IRadarrGuideService>();
builder.RegisterType<CachePersister>().As<ICachePersister>();
builder.RegisterType<CachePersisterFactory>().As<ICachePersisterFactory>();
builder.RegisterType<CachePersister>()
.As<ICachePersister>()
.InstancePerLifetimeScope();
builder.Register<Func<IServiceConfiguration, ICachePersister>>(c => config =>
{
var guidBuilderFactory = c.Resolve<Func<IServiceConfiguration, ICacheGuidBuilder>>();
return c.Resolve<CachePersister>(TypedParameter.From(guidBuilderFactory(config)));
});
// builder.Register<Func<IServiceConfiguration, ICachePersister>>(c => config =>
// {
// var guidBuilderFactory = c.Resolve<Func<IServiceConfiguration, ICacheGuidBuilder>>();
// return c.Resolve<CachePersister>(TypedParameter.From(guidBuilderFactory(config)));
// });
// Guide Processor
// todo: register as singleton to avoid parsing guide multiple times when using 2 or more instances in config
builder.RegisterType<GuideProcessor>().As<IGuideProcessor>();
builder.RegisterAggregateService<IGuideProcessorSteps>();
builder.RegisterType<CustomFormatStep>().As<ICustomFormatStep>();
builder.RegisterType<ConfigStep>().As<IConfigStep>();
builder.RegisterType<CustomFormatProcessor>().As<ICustomFormatProcessor>();
builder.RegisterType<QualityProfileStep>().As<IQualityProfileStep>();
// Persistence Processor

Loading…
Cancel
Save