recyclarr
parent
0ce95b58c0
commit
085d641e7e
@ -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,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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
Loading…
Reference in new issue