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

recyclarr
Robert Dailey 3 years ago
parent 14d87adc10
commit 461ff68730

@ -1,25 +1,41 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using TrashLib.Config;
using TrashLib.Radarr.CustomFormat.Cache;
using TrashLib.Radarr.CustomFormat.Models;
using TrashLib.Radarr.CustomFormat.Models.Cache;
namespace Recyclarr.Code.Radarr
{
internal class CustomFormatCache : ICustomFormatCache
{
private readonly IServiceConfiguration _config;
private readonly RadarrDatabaseContext _context;
public CustomFormatCache(IDbContextFactory<RadarrDatabaseContext> contextFactory)
public CustomFormatCache(IDbContextFactory<RadarrDatabaseContext> contextFactory, IServiceConfiguration config)
{
_config = config;
_context = contextFactory.CreateDbContext();
}
public IEnumerable<TrashIdMapping> Load(IServiceConfiguration config)
public IEnumerable<TrashIdMapping> Mappings =>
_context.CustomFormatCache.Where(c => c.ServiceBaseUrl == _config.BaseUrl);
public void Add(int formatId, ProcessedCustomFormatData format)
{
_context.CustomFormatCache.Add(new TrashIdMapping
{
ServiceBaseUrl = _config.BaseUrl,
CustomFormatId = formatId,
TrashId = format.TrashId
});
}
public void Remove(TrashIdMapping cfId)
{
return _context.CustomFormatCache
.Where(c => c.ServiceBaseUrl == config.BaseUrl);
_context.CustomFormatCache.Remove(cfId);
}
}
}

@ -1,4 +1,5 @@
using System.Linq;
using System;
using System.Linq;
using Fluxor;
using TrashLib.Config;
@ -6,8 +7,6 @@ namespace Recyclarr.Code.Radarr.Fluxor
{
internal record ActiveConfig<T>(T? Config) where T : IServiceConfiguration;
// internal record ActiveConfigState<T>(T? ActiveConfig);
internal class ActiveConfigFeature<T> : Feature<ActiveConfig<T>>
where T : IServiceConfiguration
{
@ -23,4 +22,13 @@ namespace Recyclarr.Code.Radarr.Fluxor
public static ActiveConfig<T> SetActiveConfig<T>(ActiveConfig<T> state, ActiveConfig<T> action)
where T : IServiceConfiguration => action;
}
// temporary until I know if Fluxor is the right tool for the job. Currently doesn't support:
// - Awaitable/Synchronous Dispatch for Events (required for LocalStorage usage in an Event from OnAfterRenderAsync())
// - Open generics for Action, State, Feature (DI probably fails?)
// public interface IActiveConfig<T> where T : IServiceConfiguration
// {
// T? Config { get; set; }
// event Action<T> ConfigChanged;
// }
}

@ -84,7 +84,7 @@ namespace Recyclarr.Code.Radarr
// Step 1.1: Optionally record deletions of custom formats in cache but not in the guide
if (config.DeleteOldCustomFormats)
{
_jsonTransactionStep.RecordDeletions(DeletedCustomFormatsInCache, radarrCfs);
_jsonTransactionStep.RecordDeletions(CustomFormats, radarrCfs);
}
// Step 2: For each merged CF, persist it to Radarr via its API. This will involve a combination of updates

@ -14,6 +14,7 @@ using Serilog;
using TrashLib.Config;
using TrashLib.Radarr;
using TrashLib.Radarr.Config;
using TrashLib.Radarr.CustomFormat.Cache;
namespace Recyclarr
{
@ -67,6 +68,10 @@ namespace Recyclarr
builder.RegisterType<RadarrConfigRepository>()
.As<IConfigRepository<RadarrConfig>>()
.InstancePerLifetimeScope();
builder.RegisterType<CustomFormatCache>()
.As<ICustomFormatCache>()
.InstancePerLifetimeScope();
}
}
}

@ -116,14 +116,14 @@
class ProfileSelectionPageManager
{
private readonly icachepersister _cachePersisterFactory;
private readonly ICustomFormatCache _cfCache;
private readonly Func<string, ICustomFormatService> _customFormatServiceFactory;
public ProfileSelectionPageManager(
ICachePersisterFactory cachePersisterFactory,
ICustomFormatCache cfCache,
Func<string, ICustomFormatService> customFormatServiceFactory)
{
_cachePersisterFactory = cachePersisterFactory;
_cfCache = cfCache;
_customFormatServiceFactory = customFormatServiceFactory;
}

@ -12,10 +12,10 @@ namespace TrashLib.Radarr.Config
IValidator<QualityDefinitionConfig> qualityDefinitionConfigValidator,
IValidator<CustomFormatConfig> customFormatConfigValidator)
{
RuleFor(x => x.BaseUrl).NotEmpty().WithMessage(messages.BaseUrl);
RuleFor(x => x.ApiKey).NotEmpty().WithMessage(messages.ApiKey);
RuleFor(x => x.QualityDefinition).SetNonNullableValidator(qualityDefinitionConfigValidator);
RuleForEach(x => x.CustomFormats).SetValidator(customFormatConfigValidator);
// RuleFor(x => x.BaseUrl).NotEmpty().WithMessage(messages.BaseUrl);
// RuleFor(x => x.ApiKey).NotEmpty().WithMessage(messages.ApiKey);
// RuleFor(x => x.QualityDefinition).SetNonNullableValidator(qualityDefinitionConfigValidator);
// RuleForEach(x => x.CustomFormats).SetValidator(customFormatConfigValidator);
}
}
@ -26,9 +26,9 @@ namespace TrashLib.Radarr.Config
IRadarrValidationMessages messages,
IValidator<QualityProfileConfig> qualityProfileConfigValidator)
{
RuleFor(x => x.Names).NotEmpty().When(x => x.TrashIds.Count == 0)
.WithMessage(messages.CustomFormatNamesAndIds);
RuleForEach(x => x.QualityProfiles).SetValidator(qualityProfileConfigValidator);
// RuleFor(x => x.Names).NotEmpty().When(x => x.TrashIds.Count == 0)
// .WithMessage(messages.CustomFormatNamesAndIds);
// RuleForEach(x => x.QualityProfiles).SetValidator(qualityProfileConfigValidator);
}
}
@ -37,7 +37,7 @@ namespace TrashLib.Radarr.Config
{
public QualityProfileConfigValidator(IRadarrValidationMessages messages)
{
RuleFor(x => x.Name).NotEmpty().WithMessage(messages.QualityProfileName);
// RuleFor(x => x.Name).NotEmpty().WithMessage(messages.QualityProfileName);
}
}
@ -46,7 +46,7 @@ namespace TrashLib.Radarr.Config
{
public QualityDefinitionConfigValidator(IRadarrValidationMessages messages)
{
RuleFor(x => x.Type).IsInEnum().WithMessage(messages.QualityDefinitionType);
// RuleFor(x => x.Type).IsInEnum().WithMessage(messages.QualityDefinitionType);
}
}
}

@ -23,28 +23,27 @@ namespace TrashLib.Radarr.CustomFormat.Api
.GetJsonAsync<List<JObject>>();
}
public async Task CreateCustomFormat(ProcessedCustomFormatData cf)
public async Task<int> CreateCustomFormat(ProcessedCustomFormatData cf)
{
var response = await BaseUrl
.AppendPathSegment("customformat")
.PostJsonAsync(cf.Json)
.ReceiveJson<JObject>();
cf.SetCache((int) response["id"]);
return (int) response["id"];
}
public async Task UpdateCustomFormat(ProcessedCustomFormatData cf)
public async Task UpdateCustomFormat(int formatId, ProcessedCustomFormatData cf)
{
await BaseUrl
.AppendPathSegment($"customformat/{cf.GetCustomFormatId()}")
.PutJsonAsync(cf.Json)
.ReceiveJson<JObject>();
.AppendPathSegment($"customformat/{formatId}")
.PutJsonAsync(cf.Json);
}
public async Task DeleteCustomFormat(int customFormatId)
public async Task DeleteCustomFormat(int formatId)
{
await BaseUrl
.AppendPathSegment($"customformat/{customFormatId}")
.AppendPathSegment($"customformat/{formatId}")
.DeleteAsync();
}
}

@ -1,15 +1,26 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using TrashLib.Radarr.CustomFormat.Models;
namespace TrashLib.Radarr.CustomFormat.Api
{
public class RadarrCustomFormatData
{
public int Id { get; set; }
public string Name { get; set; }
[JsonExtensionData, UsedImplicitly]
private JObject? _extraJson;
}
public interface ICustomFormatService
{
Task<List<JObject>> GetCustomFormats();
Task CreateCustomFormat(ProcessedCustomFormatData cf);
Task UpdateCustomFormat(ProcessedCustomFormatData cf);
Task DeleteCustomFormat(int customFormatId);
Task<List<RadarrCustomFormatData>> GetCustomFormats();
Task<int> CreateCustomFormat(ProcessedCustomFormatData cf);
Task UpdateCustomFormat(int formatId, ProcessedCustomFormatData cf);
Task DeleteCustomFormat(int formatId);
}
}

@ -1,11 +1,13 @@
using System.Collections.Generic;
using TrashLib.Config;
using TrashLib.Radarr.CustomFormat.Models;
using TrashLib.Radarr.CustomFormat.Models.Cache;
namespace TrashLib.Radarr.CustomFormat.Cache
{
public interface ICustomFormatCache
{
IEnumerable<TrashIdMapping> Load(IServiceConfiguration config);
IEnumerable<TrashIdMapping> Mappings { get; }
void Add(int formatId, ProcessedCustomFormatData format);
void Remove(TrashIdMapping cfId);
}
}

@ -2,7 +2,7 @@
{
public abstract class ServiceCacheObject
{
public int Id { get; set; }
public string ServiceBaseUrl { get; set; } = default!;
public int Id { get; init; }
public string ServiceBaseUrl { get; init; } = default!;
}
}

@ -1,275 +1,275 @@
using System;
using System.Linq;
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;
namespace TrashLib.Radarr.CustomFormat
{
internal class CustomFormatUpdater : ICustomFormatUpdater
{
private readonly ICachePersister _cache;
private readonly IGuideProcessor _guideProcessor;
private readonly IPersistenceProcessor _persistenceProcessor;
public CustomFormatUpdater(
ILogger log,
ICachePersister cache,
IGuideProcessor guideProcessor,
IPersistenceProcessor persistenceProcessor)
{
Log = log;
_cache = cache;
_guideProcessor = guideProcessor;
_persistenceProcessor = persistenceProcessor;
}
private ILogger Log { get; }
public async Task Process(bool isPreview, RadarrConfig config)
{
_cache.Load();
await _guideProcessor.BuildGuideData(config.CustomFormats, _cache.CfCache);
if (!ValidateGuideDataAndCheckShouldProceed(config))
{
return;
}
if (isPreview)
{
PreviewCustomFormats();
}
else
{
await _persistenceProcessor.PersistCustomFormats(config,
_guideProcessor.ProcessedCustomFormats,
_guideProcessor.DeletedCustomFormatsInCache,
_guideProcessor.ProfileScores);
PrintApiStatistics(_persistenceProcessor.Transactions);
PrintQualityProfileUpdates();
// Cache all the custom formats (using ID from API response).
_cache.Update(_guideProcessor.ProcessedCustomFormats);
_cache.Save();
}
_persistenceProcessor.Reset();
_guideProcessor.Reset();
}
private void PrintQualityProfileUpdates()
{
if (_persistenceProcessor.UpdatedScores.Count > 0)
{
foreach (var (profileName, scores) in _persistenceProcessor.UpdatedScores)
{
Log.Debug("> Scores updated for quality profile: {ProfileName}", profileName);
foreach (var (customFormatName, score, reason) in scores)
{
Log.Debug(" - {Format}: {Score} ({Reason})", customFormatName, score, reason);
}
}
Log.Information("Updated {ProfileCount} profiles and a total of {ScoreCount} scores",
_persistenceProcessor.UpdatedScores.Keys.Count,
_persistenceProcessor.UpdatedScores.Sum(s => s.Value.Count));
}
else
{
Log.Information("All quality profile scores are already up to date!");
}
if (_persistenceProcessor.InvalidProfileNames.Count > 0)
{
Log.Warning("The following quality profile names are not valid and should either be " +
"removed or renamed in your YAML config");
Log.Warning("{QualityProfileNames}", _persistenceProcessor.InvalidProfileNames);
}
}
private void PrintApiStatistics(CustomFormatTransactionData transactions)
{
var created = transactions.NewCustomFormats;
if (created.Count > 0)
{
Log.Information("Created {Count} New Custom Formats: {CustomFormats}", created.Count,
created.Select(r => r.Name));
}
var updated = transactions.UpdatedCustomFormats;
if (updated.Count > 0)
{
Log.Information("Updated {Count} Existing Custom Formats: {CustomFormats}", updated.Count,
updated.Select(r => r.Name));
}
var skipped = transactions.UnchangedCustomFormats;
if (skipped.Count > 0)
{
Log.Debug("Skipped {Count} Custom Formats that did not change: {CustomFormats}", skipped.Count,
skipped.Select(r => r.Name));
}
var deleted = transactions.DeletedCustomFormatIds;
if (deleted.Count > 0)
{
Log.Information("Deleted {Count} Custom Formats: {CustomFormats}", deleted.Count,
deleted.Select(r => r.CustomFormatName));
}
var totalCount = created.Count + updated.Count;
if (totalCount > 0)
{
Log.Information("Total of {Count} custom formats synced to Radarr", totalCount);
}
else
{
Log.Information("All custom formats are already up to date!");
}
}
private bool ValidateGuideDataAndCheckShouldProceed(RadarrConfig config)
{
Console.WriteLine("");
if (_guideProcessor.DuplicatedCustomFormats.Count > 0)
{
Log.Warning("One or more of the custom formats you want are duplicated in the guide. These custom " +
"formats WILL BE SKIPPED. Trash Updater is not able to choose which one you actually " +
"wanted. To resolve this ambiguity, use the `trash_ids` property in your YML " +
"configuration to refer to the custom format using its Trash ID instead of its name");
foreach (var (cfName, dupes) in _guideProcessor.DuplicatedCustomFormats)
{
Log.Warning("{CfName} is duplicated {DupeTimes} with the following Trash IDs:", cfName,
dupes.Count);
foreach (var cf in dupes)
{
Log.Warning(" - {TrashId}", cf.TrashId);
}
}
Console.WriteLine("");
}
if (_guideProcessor.CustomFormatsNotInGuide.Count > 0)
{
Log.Warning("The Custom Formats below do not exist in the guide and will " +
"be skipped. Names must match the 'name' field in the actual JSON, not the header in " +
"the guide! Either fix the names or remove them from your YAML config to resolve this " +
"warning");
Log.Warning("{CfList}", _guideProcessor.CustomFormatsNotInGuide);
Console.WriteLine("");
}
var cfsWithoutQualityProfiles = _guideProcessor.ConfigData
.Where(d => d.QualityProfiles.Count == 0)
.SelectMany(d => d.CustomFormats.Select(cf => cf.Name))
.ToList();
if (cfsWithoutQualityProfiles.Count > 0)
{
Log.Debug("These custom formats will be uploaded but are not associated to a quality profile in the " +
"config file: {UnassociatedCfs}", cfsWithoutQualityProfiles);
Console.WriteLine("");
}
// No CFs are defined in this item, or they are all invalid. Skip this whole instance.
if (_guideProcessor.ConfigData.Count == 0)
{
Log.Error("Guide processing yielded no custom formats for configured instance host {BaseUrl}",
config.BaseUrl);
return false;
}
if (_guideProcessor.CustomFormatsWithoutScore.Count > 0)
{
Log.Warning("The below custom formats have no score in the guide or YAML " +
"config and will be skipped (remove them from your config or specify a " +
"score to fix this warning)");
foreach (var tuple in _guideProcessor.CustomFormatsWithoutScore)
{
Log.Warning("{CfList}", tuple);
}
Console.WriteLine("");
}
if (_guideProcessor.CustomFormatsWithOutdatedNames.Count > 0)
{
Log.Warning("One or more custom format names in your YAML config have been renamed in the guide and " +
"are outdated. Each outdated name will be listed below. These custom formats will refuse " +
"to sync if your cache is deleted. To fix this warning, rename each one to its new name");
foreach (var (oldName, newName) in _guideProcessor.CustomFormatsWithOutdatedNames)
{
Log.Warning(" - '{OldName}' -> '{NewName}'", oldName, newName);
}
Console.WriteLine("");
}
return true;
}
private void PreviewCustomFormats()
{
Console.WriteLine("");
Console.WriteLine("=========================================================");
Console.WriteLine(" >>> Custom Formats From Guide <<< ");
Console.WriteLine("=========================================================");
Console.WriteLine("");
const string format = "{0,-30} {1,-35}";
Console.WriteLine(format, "Custom Format", "Trash ID");
Console.WriteLine(string.Concat(Enumerable.Repeat('-', 1 + 30 + 35)));
foreach (var cf in _guideProcessor.ProcessedCustomFormats)
{
Console.WriteLine(format, cf.Name, cf.TrashId);
}
Console.WriteLine("");
Console.WriteLine("=========================================================");
Console.WriteLine(" >>> Quality Profile Assignments & Scores <<< ");
Console.WriteLine("=========================================================");
Console.WriteLine("");
const string profileFormat = "{0,-18} {1,-20} {2,-8}";
Console.WriteLine(profileFormat, "Profile", "Custom Format", "Score");
Console.WriteLine(string.Concat(Enumerable.Repeat('-', 2 + 18 + 20 + 8)));
foreach (var (profileName, scoreMap) in _guideProcessor.ProfileScores)
{
Console.WriteLine(profileFormat, profileName, "", "");
foreach (var (customFormat, score) in scoreMap.Mapping)
{
var matchingCf = _guideProcessor.ProcessedCustomFormats
.FirstOrDefault(cf => cf.TrashId.EqualsIgnoreCase(customFormat.TrashId));
if (matchingCf == null)
{
Log.Warning("Quality Profile refers to CF not found in guide: {TrashId}",
customFormat.TrashId);
continue;
}
Console.WriteLine(profileFormat, "", matchingCf.Name, score);
}
}
Console.WriteLine("");
}
}
}
// using System;
// using System.Linq;
// 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;
//
// namespace TrashLib.Radarr.CustomFormat
// {
// internal class CustomFormatUpdater : ICustomFormatUpdater
// {
// private readonly ICachePersister _cache;
// private readonly IGuideProcessor _guideProcessor;
// private readonly IPersistenceProcessor _persistenceProcessor;
//
// public CustomFormatUpdater(
// ILogger log,
// ICachePersister cache,
// IGuideProcessor guideProcessor,
// IPersistenceProcessor persistenceProcessor)
// {
// Log = log;
// _cache = cache;
// _guideProcessor = guideProcessor;
// _persistenceProcessor = persistenceProcessor;
// }
//
// private ILogger Log { get; }
//
// public async Task Process(bool isPreview, RadarrConfig config)
// {
// _cache.Load();
//
// await _guideProcessor.BuildGuideData(config.CustomFormats, _cache.CfCache);
//
// if (!ValidateGuideDataAndCheckShouldProceed(config))
// {
// return;
// }
//
// if (isPreview)
// {
// PreviewCustomFormats();
// }
// else
// {
// await _persistenceProcessor.PersistCustomFormats(config,
// _guideProcessor.ProcessedCustomFormats,
// _guideProcessor.DeletedCustomFormatsInCache,
// _guideProcessor.ProfileScores);
//
// PrintApiStatistics(_persistenceProcessor.Transactions);
// PrintQualityProfileUpdates();
//
// // Cache all the custom formats (using ID from API response).
// _cache.Update(_guideProcessor.ProcessedCustomFormats);
// _cache.Save();
// }
//
// _persistenceProcessor.Reset();
// _guideProcessor.Reset();
// }
//
// private void PrintQualityProfileUpdates()
// {
// if (_persistenceProcessor.UpdatedScores.Count > 0)
// {
// foreach (var (profileName, scores) in _persistenceProcessor.UpdatedScores)
// {
// Log.Debug("> Scores updated for quality profile: {ProfileName}", profileName);
//
// foreach (var (customFormatName, score, reason) in scores)
// {
// Log.Debug(" - {Format}: {Score} ({Reason})", customFormatName, score, reason);
// }
// }
//
// Log.Information("Updated {ProfileCount} profiles and a total of {ScoreCount} scores",
// _persistenceProcessor.UpdatedScores.Keys.Count,
// _persistenceProcessor.UpdatedScores.Sum(s => s.Value.Count));
// }
// else
// {
// Log.Information("All quality profile scores are already up to date!");
// }
//
// if (_persistenceProcessor.InvalidProfileNames.Count > 0)
// {
// Log.Warning("The following quality profile names are not valid and should either be " +
// "removed or renamed in your YAML config");
// Log.Warning("{QualityProfileNames}", _persistenceProcessor.InvalidProfileNames);
// }
// }
//
// private void PrintApiStatistics(CustomFormatTransactionData transactions)
// {
// var created = transactions.NewCustomFormats;
// if (created.Count > 0)
// {
// Log.Information("Created {Count} New Custom Formats: {CustomFormats}", created.Count,
// created.Select(r => r.Name));
// }
//
// var updated = transactions.UpdatedCustomFormats;
// if (updated.Count > 0)
// {
// Log.Information("Updated {Count} Existing Custom Formats: {CustomFormats}", updated.Count,
// updated.Select(r => r.Name));
// }
//
// var skipped = transactions.UnchangedCustomFormats;
// if (skipped.Count > 0)
// {
// Log.Debug("Skipped {Count} Custom Formats that did not change: {CustomFormats}", skipped.Count,
// skipped.Select(r => r.Name));
// }
//
// var deleted = transactions.DeletedCustomFormatIds;
// if (deleted.Count > 0)
// {
// Log.Information("Deleted {Count} Custom Formats: {CustomFormats}", deleted.Count,
// deleted.Select(r => r.CustomFormatName));
// }
//
// var totalCount = created.Count + updated.Count;
// if (totalCount > 0)
// {
// Log.Information("Total of {Count} custom formats synced to Radarr", totalCount);
// }
// else
// {
// Log.Information("All custom formats are already up to date!");
// }
// }
//
// private bool ValidateGuideDataAndCheckShouldProceed(RadarrConfig config)
// {
// Console.WriteLine("");
//
// if (_guideProcessor.DuplicatedCustomFormats.Count > 0)
// {
// Log.Warning("One or more of the custom formats you want are duplicated in the guide. These custom " +
// "formats WILL BE SKIPPED. Trash Updater is not able to choose which one you actually " +
// "wanted. To resolve this ambiguity, use the `trash_ids` property in your YML " +
// "configuration to refer to the custom format using its Trash ID instead of its name");
//
// foreach (var (cfName, dupes) in _guideProcessor.DuplicatedCustomFormats)
// {
// Log.Warning("{CfName} is duplicated {DupeTimes} with the following Trash IDs:", cfName,
// dupes.Count);
// foreach (var cf in dupes)
// {
// Log.Warning(" - {TrashId}", cf.TrashId);
// }
// }
//
// Console.WriteLine("");
// }
//
// if (_guideProcessor.CustomFormatsNotInGuide.Count > 0)
// {
// Log.Warning("The Custom Formats below do not exist in the guide and will " +
// "be skipped. Names must match the 'name' field in the actual JSON, not the header in " +
// "the guide! Either fix the names or remove them from your YAML config to resolve this " +
// "warning");
// Log.Warning("{CfList}", _guideProcessor.CustomFormatsNotInGuide);
//
// Console.WriteLine("");
// }
//
// var cfsWithoutQualityProfiles = _guideProcessor.ConfigData
// .Where(d => d.QualityProfiles.Count == 0)
// .SelectMany(d => d.CustomFormats.Select(cf => cf.Name))
// .ToList();
//
// if (cfsWithoutQualityProfiles.Count > 0)
// {
// Log.Debug("These custom formats will be uploaded but are not associated to a quality profile in the " +
// "config file: {UnassociatedCfs}", cfsWithoutQualityProfiles);
//
// Console.WriteLine("");
// }
//
// // No CFs are defined in this item, or they are all invalid. Skip this whole instance.
// if (_guideProcessor.ConfigData.Count == 0)
// {
// Log.Error("Guide processing yielded no custom formats for configured instance host {BaseUrl}",
// config.BaseUrl);
// return false;
// }
//
// if (_guideProcessor.CustomFormatsWithoutScore.Count > 0)
// {
// Log.Warning("The below custom formats have no score in the guide or YAML " +
// "config and will be skipped (remove them from your config or specify a " +
// "score to fix this warning)");
// foreach (var tuple in _guideProcessor.CustomFormatsWithoutScore)
// {
// Log.Warning("{CfList}", tuple);
// }
//
// Console.WriteLine("");
// }
//
// if (_guideProcessor.CustomFormatsWithOutdatedNames.Count > 0)
// {
// Log.Warning("One or more custom format names in your YAML config have been renamed in the guide and " +
// "are outdated. Each outdated name will be listed below. These custom formats will refuse " +
// "to sync if your cache is deleted. To fix this warning, rename each one to its new name");
//
// foreach (var (oldName, newName) in _guideProcessor.CustomFormatsWithOutdatedNames)
// {
// Log.Warning(" - '{OldName}' -> '{NewName}'", oldName, newName);
// }
//
// Console.WriteLine("");
// }
//
// return true;
// }
//
// private void PreviewCustomFormats()
// {
// Console.WriteLine("");
// Console.WriteLine("=========================================================");
// Console.WriteLine(" >>> Custom Formats From Guide <<< ");
// Console.WriteLine("=========================================================");
// Console.WriteLine("");
//
// const string format = "{0,-30} {1,-35}";
// Console.WriteLine(format, "Custom Format", "Trash ID");
// Console.WriteLine(string.Concat(Enumerable.Repeat('-', 1 + 30 + 35)));
//
// foreach (var cf in _guideProcessor.ProcessedCustomFormats)
// {
// Console.WriteLine(format, cf.Name, cf.TrashId);
// }
//
// Console.WriteLine("");
// Console.WriteLine("=========================================================");
// Console.WriteLine(" >>> Quality Profile Assignments & Scores <<< ");
// Console.WriteLine("=========================================================");
// Console.WriteLine("");
//
// const string profileFormat = "{0,-18} {1,-20} {2,-8}";
// Console.WriteLine(profileFormat, "Profile", "Custom Format", "Score");
// Console.WriteLine(string.Concat(Enumerable.Repeat('-', 2 + 18 + 20 + 8)));
//
// foreach (var (profileName, scoreMap) in _guideProcessor.ProfileScores)
// {
// Console.WriteLine(profileFormat, profileName, "", "");
//
// foreach (var (customFormat, score) in scoreMap.Mapping)
// {
// var matchingCf = _guideProcessor.ProcessedCustomFormats
// .FirstOrDefault(cf => cf.TrashId.EqualsIgnoreCase(customFormat.TrashId));
//
// if (matchingCf == null)
// {
// Log.Warning("Quality Profile refers to CF not found in guide: {TrashId}",
// customFormat.TrashId);
// continue;
// }
//
// Console.WriteLine(profileFormat, "", matchingCf.Name, score);
// }
// }
//
// Console.WriteLine("");
// }
// }
// }

@ -1,10 +1,34 @@
using TrashLib.Radarr.CustomFormat.Cache;
using System;
using TrashLib.Radarr.CustomFormat.Cache;
namespace TrashLib.Radarr.CustomFormat.Models.Cache
{
public class TrashIdMapping : ServiceCacheObject
public class TrashIdMapping : ServiceCacheObject, IEquatable<TrashIdMapping>
{
public string TrashId { get; set; } = default!;
public int CustomFormatId { get; set; }
public string TrashId { get; init; } = default!;
public int CustomFormatId { get; init; }
public bool Equals(TrashIdMapping? other)
{
if (other is null)
{
return false;
}
if (ReferenceEquals(this, other))
{
return true;
}
return
TrashId == other.TrashId &&
CustomFormatId == other.CustomFormatId;
}
public override bool Equals(object? obj) =>
obj is TrashIdMapping other && Equals(other);
public override int GetHashCode() =>
HashCode.Combine(TrashId, CustomFormatId);
}
}

@ -18,16 +18,11 @@ namespace TrashLib.Radarr.CustomFormat.Models
public string TrashId { get; }
public int? Score { get; init; }
public JObject Json { get; set; }
public TrashIdMapping? CacheEntry { get; set; }
// public TrashIdMapping? CacheEntry { get; set; }
public void SetCache(int customFormatId)
{
CacheEntry = new TrashIdMapping(TrashId, customFormatId);
}
[SuppressMessage("Microsoft.Design", "CA1024", Justification = "Method throws an exception")]
public int GetCustomFormatId()
=> CacheEntry?.CustomFormatId ??
throw new InvalidOperationException("CacheEntry must exist to obtain custom format ID");
// [SuppressMessage("Microsoft.Design", "CA1024", Justification = "Method throws an exception")]
// public int GetCustomFormatId()
// => CacheEntry?.CustomFormatId ??
// throw new InvalidOperationException("CacheEntry must exist to obtain custom format ID");
}
}

@ -1,25 +1,37 @@
using System.Linq;
using System.Threading.Tasks;
using TrashLib.Radarr.Config;
using TrashLib.Radarr.CustomFormat.Api;
using TrashLib.Radarr.CustomFormat.Cache;
namespace TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps
{
internal class CustomFormatApiPersistenceStep : ICustomFormatApiPersistenceStep
{
public async Task Process(ICustomFormatService api, CustomFormatTransactionData transactions)
private readonly ICustomFormatCache _cache;
public CustomFormatApiPersistenceStep(ICustomFormatCache cache)
{
_cache = cache;
}
public async Task Process(RadarrConfig config, ICustomFormatService api, CustomFormatTransactionData transactions)
{
foreach (var cf in transactions.NewCustomFormats)
{
await api.CreateCustomFormat(cf);
var id = await api.CreateCustomFormat(cf);
_cache.Add(id, cf);
}
foreach (var cf in transactions.UpdatedCustomFormats)
{
await api.UpdateCustomFormat(cf);
await api.UpdateCustomFormat(cf.Id, cf.CustomFormat);
}
foreach (var cfId in transactions.DeletedCustomFormatIds)
{
await api.DeleteCustomFormat(cfId.CustomFormatId);
_cache.Remove(cfId);
}
}
}

@ -1,5 +1,6 @@
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
using TrashLib.Radarr.Config;
using TrashLib.Radarr.CustomFormat.Models;
using TrashLib.Radarr.CustomFormat.Models.Cache;
@ -9,8 +10,10 @@ namespace TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps
{
CustomFormatTransactionData Transactions { get; }
void Process(IEnumerable<ProcessedCustomFormatData> guideCfs,
IReadOnlyCollection<JObject> radarrCfs);
void Process(
IEnumerable<ProcessedCustomFormatData> guideCfs,
IReadOnlyCollection<JObject> radarrCfs,
RadarrConfig config);
void RecordDeletions(IEnumerable<TrashIdMapping> deletedCfsInCache, List<JObject> radarrCfs);
}

@ -1,30 +1,49 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Common.Extensions;
using Newtonsoft.Json.Linq;
using TrashLib.Config;
using TrashLib.Radarr.Config;
using TrashLib.Radarr.CustomFormat.Api;
using TrashLib.Radarr.CustomFormat.Cache;
using TrashLib.Radarr.CustomFormat.Models;
using TrashLib.Radarr.CustomFormat.Models.Cache;
namespace TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps
{
public record CachedCustomFormat(ProcessedCustomFormatData CustomFormat, int Id);
public class CustomFormatTransactionData
{
public List<ProcessedCustomFormatData> NewCustomFormats { get; } = new();
public List<ProcessedCustomFormatData> UpdatedCustomFormats { get; } = new();
public List<CachedCustomFormat> UpdatedCustomFormats { get; } = new();
public List<TrashIdMapping> DeletedCustomFormatIds { get; } = new();
public List<ProcessedCustomFormatData> UnchangedCustomFormats { get; } = new();
}
internal class JsonTransactionStep : IJsonTransactionStep
{
private readonly Func<IServiceConfiguration, ICustomFormatCache> _cacheFactory;
public JsonTransactionStep(Func<IServiceConfiguration, ICustomFormatCache> cacheFactory)
{
_cacheFactory = cacheFactory;
}
public CustomFormatTransactionData Transactions { get; } = new();
public void Process(IEnumerable<ProcessedCustomFormatData> guideCfs,
IReadOnlyCollection<JObject> radarrCfs)
public void Process(
IEnumerable<ProcessedCustomFormatData> guideCfs,
IReadOnlyCollection<JObject> radarrCfs,
RadarrConfig config)
{
foreach (var (guideCf, radarrCf) in guideCfs
.Select(gcf => (GuideCf: gcf, RadarrCf: FindRadarrCf(radarrCfs, gcf))))
var cache = _cacheFactory(config);
foreach (var guideCf in guideCfs)
{
var mapping = cache.Mappings.FirstOrDefault(m => m.TrashId == guideCf.TrashId);
var radarrCf = FindRadarrCf(radarrCfs, mapping?.CustomFormatId, guideCf.Name);
var guideCfJson = BuildNewRadarrCf(guideCf.Json);
// no match; we add this CF as brand new
@ -39,17 +58,10 @@ namespace TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps
guideCf.Json = (JObject) radarrCf.DeepClone();
UpdateRadarrCf(guideCf.Json, guideCfJson);
// Set the cache for use later (like updating scores) if it hasn't been updated already.
// This handles CFs that already exist in Radarr but aren't cached (they will be added to cache
// later).
if (guideCf.CacheEntry == null)
{
guideCf.SetCache((int) guideCf.Json["id"]);
}
if (!JToken.DeepEquals(radarrCf, guideCf.Json))
{
Transactions.UpdatedCustomFormats.Add(guideCf);
Transactions.UpdatedCustomFormats.Add(
new CachedCustomFormat(guideCf, (int) guideCf.Json["id"]));
}
else
{
@ -59,22 +71,23 @@ namespace TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps
}
}
public void RecordDeletions(IEnumerable<TrashIdMapping> deletedCfsInCache, List<JObject> radarrCfs)
public void RecordDeletions(IEnumerable<ProcessedCustomFormatData> guideCfs,
List<RadarrCustomFormatData> radarrCfs, RadarrConfig config)
{
// The 'Where' excludes cached CFs that were deleted manually by the user in Radarr
// FindRadarrCf() specifies 'null' for name because we should never delete unless an ID is found
foreach (var del in deletedCfsInCache.Where(
del => FindRadarrCf(radarrCfs, del.CustomFormatId, null) != null))
var cache = _cacheFactory(config);
foreach (var mapping in cache.Mappings
.Where(m => guideCfs.None(cf => cf.TrashId == m.TrashId)))
{
Transactions.DeletedCustomFormatIds.Add(del);
// The 'Where' excludes cached CFs that were deleted manually by the user in Radarr
var radarrCf = radarrCfs.FirstOrDefault(cf => cf.Id == mapping.CustomFormatId);
if (radarrCf != null)
{
Transactions.DeletedCustomFormatIds.Add(mapping);
}
}
}
private static JObject? FindRadarrCf(IReadOnlyCollection<JObject> radarrCfs, ProcessedCustomFormatData guideCf)
{
return FindRadarrCf(radarrCfs, guideCf.CacheEntry?.CustomFormatId, guideCf.Name);
}
private static JObject? FindRadarrCf(IReadOnlyCollection<JObject> radarrCfs, int? cfId, string? cfName)
{
JObject? match = null;

@ -32,12 +32,12 @@ namespace TrashLib.Radarr
builder.RegisterType<RadarrQualityDefinitionGuideParser>().As<IRadarrQualityDefinitionGuideParser>();
// Custom Format Support
builder.RegisterType<CustomFormatUpdater>().As<ICustomFormatUpdater>();
// builder.RegisterType<CustomFormatUpdater>().As<ICustomFormatUpdater>();
builder.RegisterType<LocalRepoCustomFormatJsonParser>().As<IRadarrGuideService>();
builder.RegisterType<CachePersisterFactory>().As<ICachePersisterFactory>();
builder.RegisterType<CachePersister>()
.As<ICachePersister>()
.InstancePerLifetimeScope();
// builder.RegisterType<CachePersisterFactory>().As<ICachePersisterFactory>();
// builder.RegisterType<CachePersister>()
// .As<ICachePersister>()
// .InstancePerLifetimeScope();
// builder.Register<Func<IServiceConfiguration, ICachePersister>>(c => config =>
// {

Loading…
Cancel
Save