using Newtonsoft.Json.Linq; using Recyclarr.Common.Extensions; using Recyclarr.TrashLib.Config.Services; using Recyclarr.TrashLib.Services.CustomFormat.Api; using Recyclarr.TrashLib.Services.CustomFormat.Models; namespace Recyclarr.TrashLib.Services.CustomFormat.Processors.PersistenceSteps; internal class QualityProfileApiPersistenceStep : IQualityProfileApiPersistenceStep { private readonly IQualityProfileService _api; private readonly List _invalidProfileNames = new(); private readonly Dictionary> _updatedScores = new(); public IDictionary> UpdatedScores => _updatedScores; public IReadOnlyCollection InvalidProfileNames => _invalidProfileNames; public QualityProfileApiPersistenceStep(IQualityProfileService api) { _api = api; } public async Task Process( IServiceConfiguration config, IDictionary cfScores) { var serviceProfiles = await _api.GetQualityProfiles(config); // Match quality profiles in Radarr to ones the user put in their config. // For each match, we return a tuple including the list of custom format scores ("formatItems"). // Using GroupJoin() because we want a LEFT OUTER JOIN so we can list which quality profiles in config // do not match profiles in Radarr. var profileScores = cfScores.GroupJoin(serviceProfiles, s => s.Key, p => p.Value("name"), (s, p) => (s.Key, s.Value, p.SelectMany(pi => pi.Children("formatItems")).ToList()), StringComparer.InvariantCultureIgnoreCase); foreach (var (profileName, scoreMap, formatItems) in profileScores) { if (formatItems.Count == 0) { _invalidProfileNames.Add(profileName); continue; } foreach (var json in formatItems) { var map = FindScoreEntry(json, scoreMap); int? scoreToUse = null; FormatScoreUpdateReason? reason = null; if (map != null) { scoreToUse = map.Score; reason = FormatScoreUpdateReason.Updated; } else if (scoreMap.ResetUnmatchedScores) { scoreToUse = 0; reason = FormatScoreUpdateReason.Reset; } if (scoreToUse == null || reason == null || json.Value("score") == scoreToUse) { continue; } json["score"] = scoreToUse.Value; _updatedScores.GetOrCreate(profileName) .Add(new UpdatedFormatScore(json.ValueOrThrow("name"), scoreToUse.Value, reason.Value)); } if (!_updatedScores.TryGetValue(profileName, out var updatedScores) || updatedScores.Count == 0) { // No scores to update, so don't bother with the API call continue; } var jsonRoot = (JObject) formatItems.First().Root; await _api.UpdateQualityProfile(config, jsonRoot, jsonRoot.Value("id")); } } private static FormatMappingEntry? FindScoreEntry(JObject formatItem, QualityProfileCustomFormatScoreMapping scoreMap) { return scoreMap.Mapping.FirstOrDefault( m => formatItem.Value("format") == m.CustomFormat.FormatId); } }