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

recyclarr
Robert Dailey 3 years ago
parent 461ff68730
commit 8b45379cc8

@ -1,4 +1,4 @@
# noinspection EditorConfigKeyCorrectness
[*] [*]
charset = utf-8 charset = utf-8
end_of_line = crlf end_of_line = crlf
@ -919,7 +919,7 @@ resharper_event_handler_pattern_short = On$event$
resharper_expression_braces = inside resharper_expression_braces = inside
resharper_expression_pars = inside resharper_expression_pars = inside
resharper_extra_spaces = remove_all resharper_extra_spaces = remove_all
resharper_force_attribute_style = separate resharper_force_attribute_style = join
resharper_force_chop_compound_do_expression = false resharper_force_chop_compound_do_expression = false
resharper_force_chop_compound_if_expression = false resharper_force_chop_compound_if_expression = false
resharper_force_chop_compound_while_expression = false resharper_force_chop_compound_while_expression = false

@ -1,4 +1,3 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -33,9 +32,7 @@ namespace Recyclarr.Code.Radarr
}); });
} }
public void Remove(TrashIdMapping cfId) public void Remove(TrashIdMapping cfId) => _context.CustomFormatCache.Remove(cfId);
{ public void Save() => _context.SaveChanges();
_context.CustomFormatCache.Remove(cfId);
}
} }
} }

@ -79,7 +79,7 @@ namespace Recyclarr.Code.Radarr
// Match CFs between the guide & Radarr and merge the data. The goal is to retain as much of the // 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 // 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. // directly care about. We keep those and just update the ones we do care about.
_jsonTransactionStep.Process(CustomFormats, radarrCfs); _jsonTransactionStep.Process(CustomFormats, radarrCfs, config);
// Step 1.1: Optionally record deletions of custom formats in cache but not in the guide // Step 1.1: Optionally record deletions of custom formats in cache but not in the guide
if (config.DeleteOldCustomFormats) if (config.DeleteOldCustomFormats)

@ -9,8 +9,8 @@ namespace TrashLib.Radarr.Config
public class RadarrConfig : ServiceConfiguration public class RadarrConfig : ServiceConfiguration
{ {
public QualityDefinitionConfig? QualityDefinition { get; init; } public QualityDefinitionConfig? QualityDefinition { get; init; }
public ICollection<CustomFormatConfig> CustomFormats { get; set; } // = new(); public List<CustomFormatConfig> CustomFormats { get; init; } = new();
public ICollection<QualityProfileConfig> QualityProfiles { get; init; } // = new(); public List<QualityProfileConfig> QualityProfiles { get; init; } = new();
public bool DeleteOldCustomFormats { get; init; } public bool DeleteOldCustomFormats { get; init; }
} }
@ -22,7 +22,8 @@ namespace TrashLib.Radarr.Config
public class QualityProfileConfig public class QualityProfileConfig
{ {
public ICollection<ScoreEntryConfig> Scores { get; init; } // = new(); public string ProfileName { get; init; } = "";
public List<ScoreEntryConfig> Scores { get; init; } = new();
public bool ResetUnmatchedScores { get; init; } public bool ResetUnmatchedScores { get; init; }
} }

@ -1,24 +1,13 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations; using TrashLib.Radarr.CustomFormat.Api.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Models;
namespace TrashLib.Radarr.CustomFormat.Api 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 public interface ICustomFormatService
{ {
Task<List<RadarrCustomFormatData>> GetCustomFormats(); Task<List<CustomFormatData>> GetCustomFormats();
Task<int> CreateCustomFormat(ProcessedCustomFormatData cf); Task<int> CreateCustomFormat(ProcessedCustomFormatData cf);
Task UpdateCustomFormat(int formatId, ProcessedCustomFormatData cf); Task UpdateCustomFormat(int formatId, ProcessedCustomFormatData cf);
Task DeleteCustomFormat(int formatId); Task DeleteCustomFormat(int formatId);

@ -0,0 +1,25 @@
using System.Collections.Generic;
using JetBrains.Annotations;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace TrashLib.Radarr.CustomFormat.Api.Models
{
public class CustomFormatData
{
public int Id { get; set; }
public string Name { get; set; }
public List<SpecificationData> Specifications { get; set; } = new();
[JsonExtensionData, UsedImplicitly]
public JObject? ExtraJson { get; init; }
}
public class SpecificationData
{
public string Name { get; set; }
[JsonExtensionData, UsedImplicitly]
public JObject? ExtraJson { get; init; }
}
}

@ -9,5 +9,6 @@ namespace TrashLib.Radarr.CustomFormat.Cache
IEnumerable<TrashIdMapping> Mappings { get; } IEnumerable<TrashIdMapping> Mappings { get; }
void Add(int formatId, ProcessedCustomFormatData format); void Add(int formatId, ProcessedCustomFormatData format);
void Remove(TrashIdMapping cfId); void Remove(TrashIdMapping cfId);
void Save();
} }
} }

@ -1,28 +1,39 @@
using System; using Newtonsoft.Json;
using System.Diagnostics.CodeAnalysis; using TrashLib.Radarr.CustomFormat.Api.Models;
using Newtonsoft.Json.Linq;
using TrashLib.Radarr.CustomFormat.Models.Cache;
namespace TrashLib.Radarr.CustomFormat.Models namespace TrashLib.Radarr.CustomFormat.Models
{ {
public class ProcessedCustomFormatData public class ProcessedCustomFormatData
{ {
public ProcessedCustomFormatData(string name, string trashId, JObject json) public ProcessedCustomFormatData(string trashId, CustomFormatData data)
{ {
Name = name;
TrashId = trashId; TrashId = trashId;
Json = json; Data = data;
} }
public string Name { get; } public string Name => Data.Name;
public string TrashId { get; } public string TrashId { get; }
public int? Score { get; init; } public int? Score { get; init; }
public JObject Json { get; set; }
// public TrashIdMapping? CacheEntry { get; set; }
// [SuppressMessage("Microsoft.Design", "CA1024", Justification = "Method throws an exception")] public CustomFormatData Data { get; }
// public int GetCustomFormatId()
// => CacheEntry?.CustomFormatId ?? public static ProcessedCustomFormatData CreateFromJson(string guideData)
// throw new InvalidOperationException("CacheEntry must exist to obtain custom format ID"); {
var cfData = JsonConvert.DeserializeObject<CustomFormatData>(guideData);
var trashId = (string) cfData.ExtraJson["trash_id"];
int? finalScore = null;
if (cfData.ExtraJson.TryGetValue("trash_score", out var score))
{
finalScore = (int) score;
cfData.ExtraJson.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
cfData.ExtraJson.Property("trash_id").Remove();
return new ProcessedCustomFormatData(trashId, cfData) {Score = finalScore};
}
} }
} }

@ -1,31 +1,19 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Newtonsoft.Json.Linq;
using TrashLib.Config;
using TrashLib.Radarr.Config; using TrashLib.Radarr.Config;
using TrashLib.Radarr.CustomFormat.Cache;
using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Models;
using TrashLib.Radarr.CustomFormat.Models.Cache;
namespace TrashLib.Radarr.CustomFormat.Processors.GuideSteps namespace TrashLib.Radarr.CustomFormat.Processors.GuideSteps
{ {
internal class CustomFormatProcessor : ICustomFormatProcessor internal class CustomFormatProcessor : ICustomFormatProcessor
{ {
private readonly ICustomFormatCache _cache;
public CustomFormatProcessor(ICustomFormatCache cache)
{
_cache = cache;
}
public List<ProcessedCustomFormatData> CustomFormats { get; } = new(); public List<ProcessedCustomFormatData> CustomFormats { get; } = new();
public List<TrashIdMapping> DeletedCustomFormatsInCache { get; } = new();
public void Process(IEnumerable<string> customFormatGuideData, RadarrConfig config) public void Process(IEnumerable<string> customFormatGuideData, RadarrConfig config)
{ {
var processedCfs = customFormatGuideData var processedCfs = customFormatGuideData
.Select(jsonData => ProcessCustomFormatData(jsonData, config)) .Select(ProcessedCustomFormatData.CreateFromJson)
.ToList(); .ToList();
// For each ID listed under the `trash_ids` YML property, match it to an existing CF // For each ID listed under the `trash_ids` YML property, match it to an existing CF
@ -37,45 +25,6 @@ namespace TrashLib.Radarr.CustomFormat.Processors.GuideSteps
cf => cf.TrashId, cf => cf.TrashId,
(_, cf) => cf, (_, cf) => cf,
StringComparer.InvariantCultureIgnoreCase)); StringComparer.InvariantCultureIgnoreCase));
// Orphaned entries in cache represent custom formats we need to delete.
ProcessDeletedCustomFormats(config);
}
private ProcessedCustomFormatData ProcessCustomFormatData(
string guideData,
IServiceConfiguration config)
{
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.Load(config).FirstOrDefault(c => c.TrashId == trashId)
};
}
private void ProcessDeletedCustomFormats(RadarrConfig config)
{
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.Load(config)
.Where(c => !CustomFormats.Any(cf => MatchCfInCache(cf, c))));
} }
} }
} }

@ -1,15 +1,12 @@
using System.Collections.Generic; using System.Collections.Generic;
using TrashLib.Radarr.Config; using TrashLib.Radarr.Config;
using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Models;
using TrashLib.Radarr.CustomFormat.Models.Cache;
namespace TrashLib.Radarr.CustomFormat.Processors.GuideSteps namespace TrashLib.Radarr.CustomFormat.Processors.GuideSteps
{ {
public interface ICustomFormatProcessor public interface ICustomFormatProcessor
{ {
List<ProcessedCustomFormatData> CustomFormats { get; } List<ProcessedCustomFormatData> CustomFormats { get; }
List<TrashIdMapping> DeletedCustomFormatsInCache { get; }
void Process(IEnumerable<string> customFormatGuideData, RadarrConfig config); void Process(IEnumerable<string> customFormatGuideData, RadarrConfig config);
} }
} }

@ -1,4 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using TrashLib.Radarr.Config;
using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Models;
namespace TrashLib.Radarr.CustomFormat.Processors.GuideSteps namespace TrashLib.Radarr.CustomFormat.Processors.GuideSteps
@ -8,22 +10,22 @@ namespace TrashLib.Radarr.CustomFormat.Processors.GuideSteps
public Dictionary<string, QualityProfileCustomFormatScoreMapping> ProfileScores { get; } = new(); public Dictionary<string, QualityProfileCustomFormatScoreMapping> ProfileScores { get; } = new();
public List<(string name, string trashId, string profileName)> CustomFormatsWithoutScore { get; } = new(); public List<(string name, string trashId, string profileName)> CustomFormatsWithoutScore { get; } = new();
public void Process(IEnumerable<ProcessedConfigData> configData) public void Process(RadarrConfig config, IReadOnlyCollection<ProcessedCustomFormatData> customFormats)
{ {
foreach (var config in configData) // foreach (var config in configData)
foreach (var profile in config.QualityProfiles) foreach (var profile in config.QualityProfiles)
foreach (var cf in config.CustomFormats) foreach (var cfScore in profile.Scores)
{ {
// Check if there is a score we can use. Priority is: // Check if there is a score we can use. Priority is:
// 1. Score from the YAML config is used. If user did not provide, // 1. Score from the YAML config is used. If user did not provide,
// 2. Score from the guide is used. If the guide did not have one, // 2. Score from the guide is used. If the guide did not have one,
// 3. Warn the user and // 3. Warn the user and
var scoreToUse = profile.Score; var scoreToUse = profile.Scores.FirstOrDefault(s => s.TrashId == cf.TrashId);
if (scoreToUse == null) if (scoreToUse == null)
{ {
if (cf.Score == null) if (cf.Score == null)
{ {
CustomFormatsWithoutScore.Add((cf.Name, cf.TrashId, profile.Name)); CustomFormatsWithoutScore.Add((cf.Name, cf.TrashId, profile.));
} }
else else
{ {

@ -1,81 +1,81 @@
using System; // using System;
using System.Collections.Generic; // using System.Collections.Generic;
using System.Threading.Tasks; // using System.Threading.Tasks;
using TrashLib.Radarr.Config; // using TrashLib.Radarr.Config;
using TrashLib.Radarr.CustomFormat.Api; // using TrashLib.Radarr.CustomFormat.Api;
using TrashLib.Radarr.CustomFormat.Models; // using TrashLib.Radarr.CustomFormat.Models;
using TrashLib.Radarr.CustomFormat.Models.Cache; // using TrashLib.Radarr.CustomFormat.Models.Cache;
using TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps; // using TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps;
//
namespace TrashLib.Radarr.CustomFormat.Processors // namespace TrashLib.Radarr.CustomFormat.Processors
{ // {
public interface IPersistenceProcessorSteps // public interface IPersistenceProcessorSteps
{ // {
public IJsonTransactionStep JsonTransactionStep { get; } // public IJsonTransactionStep JsonTransactionStep { get; }
public ICustomFormatApiPersistenceStep CustomFormatCustomFormatApiPersister { get; } // public ICustomFormatApiPersistenceStep CustomFormatCustomFormatApiPersister { get; }
public IQualityProfileApiPersistenceStep ProfileQualityProfileApiPersister { get; } // public IQualityProfileApiPersistenceStep ProfileQualityProfileApiPersister { get; }
} // }
//
internal class PersistenceProcessor : IPersistenceProcessor // internal class PersistenceProcessor : IPersistenceProcessor
{ // {
private readonly Func<string, ICustomFormatService> _customFormatServiceFactory; // private readonly Func<string, ICustomFormatService> _customFormatServiceFactory;
private readonly Func<string, IQualityProfileService> _qualityProfileServiceFactory; // private readonly Func<string, IQualityProfileService> _qualityProfileServiceFactory;
private readonly Func<IPersistenceProcessorSteps> _stepsFactory; // private readonly Func<IPersistenceProcessorSteps> _stepsFactory;
private IPersistenceProcessorSteps _steps; // private IPersistenceProcessorSteps _steps;
//
public PersistenceProcessor( // public PersistenceProcessor(
Func<string, ICustomFormatService> customFormatServiceFactory, // Func<string, ICustomFormatService> customFormatServiceFactory,
Func<string, IQualityProfileService> qualityProfileServiceFactory, // Func<string, IQualityProfileService> qualityProfileServiceFactory,
Func<IPersistenceProcessorSteps> stepsFactory) // Func<IPersistenceProcessorSteps> stepsFactory)
{ // {
_qualityProfileServiceFactory = qualityProfileServiceFactory; // _qualityProfileServiceFactory = qualityProfileServiceFactory;
_stepsFactory = stepsFactory; // _stepsFactory = stepsFactory;
_customFormatServiceFactory = customFormatServiceFactory; // _customFormatServiceFactory = customFormatServiceFactory;
_steps = _stepsFactory(); // _steps = _stepsFactory();
} // }
//
public CustomFormatTransactionData Transactions // public CustomFormatTransactionData Transactions
=> _steps.JsonTransactionStep.Transactions; // => _steps.JsonTransactionStep.Transactions;
//
public IDictionary<string, List<UpdatedFormatScore>> UpdatedScores // public IDictionary<string, List<UpdatedFormatScore>> UpdatedScores
=> _steps.ProfileQualityProfileApiPersister.UpdatedScores; // => _steps.ProfileQualityProfileApiPersister.UpdatedScores;
//
public IReadOnlyCollection<string> InvalidProfileNames // public IReadOnlyCollection<string> InvalidProfileNames
=> _steps.ProfileQualityProfileApiPersister.InvalidProfileNames; // => _steps.ProfileQualityProfileApiPersister.InvalidProfileNames;
//
public void Reset() // public void Reset()
{ // {
_steps = _stepsFactory(); // _steps = _stepsFactory();
} // }
//
public async Task PersistCustomFormats( // public async Task PersistCustomFormats(
RadarrConfig config, // RadarrConfig config,
IEnumerable<ProcessedCustomFormatData> guideCfs, // IEnumerable<ProcessedCustomFormatData> guideCfs,
IEnumerable<TrashIdMapping> deletedCfsInCache, // IEnumerable<TrashIdMapping> deletedCfsInCache,
IDictionary<string, QualityProfileCustomFormatScoreMapping> profileScores) // IDictionary<string, QualityProfileCustomFormatScoreMapping> profileScores)
{ // {
var customFormatService = _customFormatServiceFactory(config.BuildUrl()); // var customFormatService = _customFormatServiceFactory(config.BuildUrl());
var radarrCfs = await customFormatService.GetCustomFormats(); // var radarrCfs = await customFormatService.GetCustomFormats();
//
// Step 1: Match CFs between the guide & Radarr and merge the data. The goal is to retain as much of the // // Step 1: 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 // // 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. // // directly care about. We keep those and just update the ones we do care about.
_steps.JsonTransactionStep.Process(guideCfs, radarrCfs); // _steps.JsonTransactionStep.Process(guideCfs, radarrCfs);
//
// Step 1.1: Optionally record deletions of custom formats in cache but not in the guide // // Step 1.1: Optionally record deletions of custom formats in cache but not in the guide
if (config.DeleteOldCustomFormats) // if (config.DeleteOldCustomFormats)
{ // {
_steps.JsonTransactionStep.RecordDeletions(deletedCfsInCache, radarrCfs); // _steps.JsonTransactionStep.RecordDeletions(deletedCfsInCache, radarrCfs);
} // }
//
// Step 2: For each merged CF, persist it to Radarr via its API. This will involve a combination of updates // // 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. // // to existing CFs and creation of brand new ones, depending on what's already available in Radarr.
await _steps.CustomFormatCustomFormatApiPersister.Process( // await _steps.CustomFormatCustomFormatApiPersister.Process(
customFormatService, _steps.JsonTransactionStep.Transactions); // customFormatService, _steps.JsonTransactionStep.Transactions);
//
// Step 3: Update all quality profiles with the scores from the guide for the uploaded custom formats // // Step 3: Update all quality profiles with the scores from the guide for the uploaded custom formats
await _steps.ProfileQualityProfileApiPersister.Process( // await _steps.ProfileQualityProfileApiPersister.Process(
_qualityProfileServiceFactory(config.BuildUrl()), profileScores); // _qualityProfileServiceFactory(config.BuildUrl()), profileScores);
} // }
} // }
} // }

@ -1,5 +1,6 @@
using System.Linq; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using TrashLib.Config;
using TrashLib.Radarr.Config; using TrashLib.Radarr.Config;
using TrashLib.Radarr.CustomFormat.Api; using TrashLib.Radarr.CustomFormat.Api;
using TrashLib.Radarr.CustomFormat.Cache; using TrashLib.Radarr.CustomFormat.Cache;
@ -8,31 +9,36 @@ namespace TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps
{ {
internal class CustomFormatApiPersistenceStep : ICustomFormatApiPersistenceStep internal class CustomFormatApiPersistenceStep : ICustomFormatApiPersistenceStep
{ {
private readonly ICustomFormatCache _cache; private readonly Func<IServiceConfiguration, ICustomFormatCache> _cacheFactory;
public CustomFormatApiPersistenceStep(ICustomFormatCache cache) public CustomFormatApiPersistenceStep(Func<IServiceConfiguration, ICustomFormatCache> cacheFactory)
{ {
_cache = cache; _cacheFactory = cacheFactory;
} }
public async Task Process(RadarrConfig config, ICustomFormatService api, CustomFormatTransactionData transactions) public async Task Process(RadarrConfig config, ICustomFormatService api,
CustomFormatTransactionData transactions)
{ {
var cache = _cacheFactory(config);
foreach (var cf in transactions.NewCustomFormats) foreach (var cf in transactions.NewCustomFormats)
{ {
var id = await api.CreateCustomFormat(cf); var id = await api.CreateCustomFormat(cf);
_cache.Add(id, cf); cache.Add(id, cf);
} }
foreach (var cf in transactions.UpdatedCustomFormats) foreach (var (customFormat, id) in transactions.UpdatedCustomFormats)
{ {
await api.UpdateCustomFormat(cf.Id, cf.CustomFormat); await api.UpdateCustomFormat(id, customFormat);
} }
foreach (var cfId in transactions.DeletedCustomFormatIds) foreach (var cfId in transactions.DeletedCustomFormatIds)
{ {
await api.DeleteCustomFormat(cfId.CustomFormatId); await api.DeleteCustomFormat(cfId.CustomFormatId);
_cache.Remove(cfId); cache.Remove(cfId);
} }
cache.Save();
} }
} }
} }

@ -5,6 +5,5 @@ namespace TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps
{ {
public interface ICustomFormatApiPersistenceStep public interface ICustomFormatApiPersistenceStep
{ {
Task Process(ICustomFormatService api, CustomFormatTransactionData transactions);
} }
} }

@ -8,13 +8,5 @@ namespace TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps
{ {
public interface IJsonTransactionStep public interface IJsonTransactionStep
{ {
CustomFormatTransactionData Transactions { get; }
void Process(
IEnumerable<ProcessedCustomFormatData> guideCfs,
IReadOnlyCollection<JObject> radarrCfs,
RadarrConfig config);
void RecordDeletions(IEnumerable<TrashIdMapping> deletedCfsInCache, List<JObject> radarrCfs);
} }
} }

@ -5,7 +5,7 @@ using Common.Extensions;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using TrashLib.Config; using TrashLib.Config;
using TrashLib.Radarr.Config; using TrashLib.Radarr.Config;
using TrashLib.Radarr.CustomFormat.Api; using TrashLib.Radarr.CustomFormat.Api.Models;
using TrashLib.Radarr.CustomFormat.Cache; using TrashLib.Radarr.CustomFormat.Cache;
using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Models;
using TrashLib.Radarr.CustomFormat.Models.Cache; using TrashLib.Radarr.CustomFormat.Models.Cache;
@ -35,7 +35,7 @@ namespace TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps
public void Process( public void Process(
IEnumerable<ProcessedCustomFormatData> guideCfs, IEnumerable<ProcessedCustomFormatData> guideCfs,
IReadOnlyCollection<JObject> radarrCfs, IReadOnlyCollection<CustomFormatData> radarrCfs,
RadarrConfig config) RadarrConfig config)
{ {
var cache = _cacheFactory(config); var cache = _cacheFactory(config);
@ -44,24 +44,22 @@ namespace TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps
{ {
var mapping = cache.Mappings.FirstOrDefault(m => m.TrashId == guideCf.TrashId); var mapping = cache.Mappings.FirstOrDefault(m => m.TrashId == guideCf.TrashId);
var radarrCf = FindRadarrCf(radarrCfs, mapping?.CustomFormatId, guideCf.Name); var radarrCf = FindRadarrCf(radarrCfs, mapping?.CustomFormatId, guideCf.Name);
var guideCfJson = BuildNewRadarrCf(guideCf.Json); FixupRadarrCf(guideCf.Data);
// no match; we add this CF as brand new // no match; we add this CF as brand new
if (radarrCf == null) if (radarrCf == null)
{ {
guideCf.Json = guideCfJson;
Transactions.NewCustomFormats.Add(guideCf); Transactions.NewCustomFormats.Add(guideCf);
} }
// found match in radarr CFs; update the existing CF // found match in radarr CFs; update the existing CF
else else
{ {
guideCf.Json = (JObject) radarrCf.DeepClone(); var originalRadarrJson = JObject.FromObject(radarrCf);
UpdateRadarrCf(guideCf.Json, guideCfJson); UpdateRadarrCf(radarrCf, guideCf.Data);
if (!JToken.DeepEquals(radarrCf, guideCf.Json)) if (!JToken.DeepEquals(JObject.FromObject(radarrCf), originalRadarrJson))
{ {
Transactions.UpdatedCustomFormats.Add( Transactions.UpdatedCustomFormats.Add(new CachedCustomFormat(guideCf, guideCf.Data.Id));
new CachedCustomFormat(guideCf, (int) guideCf.Json["id"]));
} }
else else
{ {
@ -72,7 +70,7 @@ namespace TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps
} }
public void RecordDeletions(IEnumerable<ProcessedCustomFormatData> guideCfs, public void RecordDeletions(IEnumerable<ProcessedCustomFormatData> guideCfs,
List<RadarrCustomFormatData> radarrCfs, RadarrConfig config) List<CustomFormatData> radarrCfs, RadarrConfig config)
{ {
var cache = _cacheFactory(config); var cache = _cacheFactory(config);
@ -88,60 +86,64 @@ namespace TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps
} }
} }
private static JObject? FindRadarrCf(IReadOnlyCollection<JObject> radarrCfs, int? cfId, string? cfName) private static CustomFormatData? FindRadarrCf(IReadOnlyCollection<CustomFormatData> radarrCfs,
int? cfId, string? cfName)
{ {
JObject? match = null; CustomFormatData? match = null;
// Try to find match in cache first // Try to find match in cache first
if (cfId != null) if (cfId != null)
{ {
match = radarrCfs.FirstOrDefault(rcf => cfId == rcf["id"].Value<int>()); match = radarrCfs.FirstOrDefault(rcf => cfId == rcf.Id);
} }
// If we don't find by ID, search by name (if a name was given) // If we don't find by ID, search by name (if a name was given)
if (match == null && cfName != null) if (match == null && cfName != null)
{ {
match = radarrCfs.FirstOrDefault(rcf => cfName.EqualsIgnoreCase(rcf["name"].Value<string>())); match = radarrCfs.FirstOrDefault(rcf => cfName.EqualsIgnoreCase(rcf.Name));
} }
return match; return match;
} }
private static void UpdateRadarrCf(JObject cfToModify, JObject cfToMergeFrom) private static void UpdateRadarrCf(CustomFormatData cfToModify, CustomFormatData cfToMergeFrom)
{ {
MergeProperties(cfToModify, cfToMergeFrom, JTokenType.Array); MergeProperties(cfToModify.ExtraJson, cfToMergeFrom.ExtraJson);
var radarrSpecs = cfToModify["specifications"].Children<JObject>(); var radarrSpecs = cfToModify.Specifications;
var guideSpecs = cfToMergeFrom["specifications"].Children<JObject>(); var guideSpecs = cfToMergeFrom.Specifications;
var matchedGuideSpecs = guideSpecs var matchedGuideSpecs = guideSpecs
.GroupBy(gs => radarrSpecs.FirstOrDefault(gss => KeyMatch(gss, gs, "name"))) .GroupBy(gs => radarrSpecs.FirstOrDefault(gss => gss.Name == gs.Name))
.SelectMany(kvp => kvp.Select(gs => new {GuideSpec = gs, RadarrSpec = kvp.Key})); .SelectMany(kvp => kvp.Select(gs => new {GuideSpec = gs, RadarrSpec = kvp.Key}));
var newRadarrSpecs = new JArray(); cfToModify.Specifications.Clear();
foreach (var match in matchedGuideSpecs) foreach (var match in matchedGuideSpecs)
{ {
if (match.RadarrSpec != null) if (match.RadarrSpec != null)
{ {
MergeProperties(match.RadarrSpec, match.GuideSpec); MergeProperties(match.RadarrSpec.ExtraJson, match.GuideSpec.ExtraJson);
newRadarrSpecs.Add(match.RadarrSpec); cfToModify.Specifications.Add(match.RadarrSpec);
} }
else else
{ {
newRadarrSpecs.Add(match.GuideSpec); cfToModify.Specifications.Add(match.GuideSpec);
} }
} }
cfToModify["specifications"] = newRadarrSpecs;
} }
private static bool KeyMatch(JObject left, JObject right, string keyName) // private static bool KeyMatch(CustomFormatData left, CustomFormatData right, string keyName)
=> left[keyName].Value<string>() == right[keyName].Value<string>(); // => left[keyName].Value<string>() == right[keyName].Value<string>();
private static void MergeProperties(JObject radarrCf, JObject guideCfJson, private static void MergeProperties(JObject? radarrCf, JObject? guideCfJson,
JTokenType exceptType = JTokenType.None) JTokenType exceptType = JTokenType.None)
{ {
if (radarrCf == null || guideCfJson == null)
{
return;
}
foreach (var guideProp in guideCfJson.Properties().Where(p => p.Value.Type != exceptType)) foreach (var guideProp in guideCfJson.Properties().Where(p => p.Value.Type != exceptType))
{ {
if (guideProp.Value.Type == JTokenType.Array && if (guideProp.Value.Type == JTokenType.Array &&
@ -159,7 +161,7 @@ namespace TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps
} }
} }
private static JObject BuildNewRadarrCf(JObject jsonPayload) private static void FixupRadarrCf(CustomFormatData cfData)
{ {
// Information on required fields from nitsua // Information on required fields from nitsua
/* /*
@ -169,17 +171,20 @@ namespace TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps
everything else radarr can handle with backend logic everything else radarr can handle with backend logic
*/ */
foreach (var child in jsonPayload["specifications"]) foreach (var spec in cfData.Specifications)
{ {
if (spec.ExtraJson is null)
{
continue;
}
// convert from `"fields": {}` to `"fields": [{}]` (object to array of object) // convert from `"fields": {}` to `"fields": [{}]` (object to array of object)
// Weirdly the exported version of a custom format is not in array form, but the API requires the array // Weirdly the exported version of a custom format is not in array form, but the API requires the array
// even if there's only one element. // even if there's only one element.
var field = child["fields"]; var field = spec.ExtraJson["fields"];
field["name"] = "value"; field["name"] = "value";
child["fields"] = new JArray {field}; spec.ExtraJson["fields"] = new JArray {field};
} }
return jsonPayload;
} }
} }
} }

Loading…
Cancel
Save