parent
bc485a8ac2
commit
13b8e5679e
@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$id": "https://raw.githubusercontent.com/recyclarr/recyclarr/master/schemas/config/media-naming-radarr.json",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"folder": { "type": "string" },
|
||||
"movie": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"rename": { "type": "boolean" },
|
||||
"format": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$id": "https://raw.githubusercontent.com/recyclarr/recyclarr/master/schemas/config/media-naming-sonarr.json",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"season": { "type": "string" },
|
||||
"series": { "type": "string" },
|
||||
"episodes": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"rename": { "type": "boolean" },
|
||||
"standard": { "type": "string" },
|
||||
"daily": { "type": "string" },
|
||||
"anime": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
using Autofac;
|
||||
using Autofac.Extras.AggregateService;
|
||||
using Recyclarr.Cli.Pipelines.MediaNaming.PipelinePhases;
|
||||
|
||||
namespace Recyclarr.Cli.Pipelines.MediaNaming;
|
||||
|
||||
public class MediaNamingAutofacModule : Module
|
||||
{
|
||||
protected override void Load(ContainerBuilder builder)
|
||||
{
|
||||
base.Load(builder);
|
||||
|
||||
builder.RegisterAggregateService<IMediaNamingPipelinePhases>();
|
||||
builder.RegisterType<MediaNamingConfigPhase>();
|
||||
builder.RegisterType<MediaNamingApiFetchPhase>();
|
||||
builder.RegisterType<MediaNamingTransactionPhase>();
|
||||
builder.RegisterType<MediaNamingPreviewPhase>();
|
||||
builder.RegisterType<MediaNamingApiPersistencePhase>();
|
||||
builder.RegisterType<MediaNamingPhaseLogger>();
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
using Recyclarr.Cli.Console.Settings;
|
||||
using Recyclarr.Cli.Pipelines.MediaNaming.PipelinePhases;
|
||||
using Recyclarr.Config.Models;
|
||||
|
||||
namespace Recyclarr.Cli.Pipelines.MediaNaming;
|
||||
|
||||
public interface IMediaNamingPipelinePhases
|
||||
{
|
||||
MediaNamingConfigPhase ConfigPhase { get; }
|
||||
MediaNamingPhaseLogger Logger { get; }
|
||||
MediaNamingApiFetchPhase ApiFetchPhase { get; }
|
||||
MediaNamingTransactionPhase TransactionPhase { get; }
|
||||
MediaNamingPreviewPhase PreviewPhase { get; }
|
||||
MediaNamingApiPersistencePhase ApiPersistencePhase { get; }
|
||||
}
|
||||
|
||||
public class MediaNamingSyncPipeline : ISyncPipeline
|
||||
{
|
||||
private readonly IMediaNamingPipelinePhases _phases;
|
||||
|
||||
public MediaNamingSyncPipeline(IMediaNamingPipelinePhases phases)
|
||||
{
|
||||
_phases = phases;
|
||||
}
|
||||
|
||||
public async Task Execute(ISyncSettings settings, IServiceConfiguration config)
|
||||
{
|
||||
var processedNaming = await _phases.ConfigPhase.Execute(config);
|
||||
if (_phases.Logger.LogConfigPhaseAndExitIfNeeded(processedNaming))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var serviceData = await _phases.ApiFetchPhase.Execute(config);
|
||||
|
||||
var transactions = _phases.TransactionPhase.Execute(serviceData, processedNaming);
|
||||
|
||||
if (settings.Preview)
|
||||
{
|
||||
_phases.PreviewPhase.Execute(transactions);
|
||||
return;
|
||||
}
|
||||
|
||||
await _phases.ApiPersistencePhase.Execute(config, transactions);
|
||||
_phases.Logger.LogPersistenceResults(serviceData, transactions);
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
using Recyclarr.Config.Models;
|
||||
using Recyclarr.ServarrApi.MediaNaming;
|
||||
|
||||
namespace Recyclarr.Cli.Pipelines.MediaNaming.PipelinePhases;
|
||||
|
||||
public class MediaNamingApiFetchPhase
|
||||
{
|
||||
private readonly IMediaNamingApiService _api;
|
||||
|
||||
public MediaNamingApiFetchPhase(IMediaNamingApiService api)
|
||||
{
|
||||
_api = api;
|
||||
}
|
||||
|
||||
public async Task<MediaNamingDto> Execute(IServiceConfiguration config)
|
||||
{
|
||||
return await _api.GetNaming(config);
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
using Recyclarr.Config.Models;
|
||||
using Recyclarr.ServarrApi.MediaNaming;
|
||||
|
||||
namespace Recyclarr.Cli.Pipelines.MediaNaming.PipelinePhases;
|
||||
|
||||
public class MediaNamingApiPersistencePhase
|
||||
{
|
||||
private readonly IMediaNamingApiService _api;
|
||||
|
||||
public MediaNamingApiPersistencePhase(IMediaNamingApiService api)
|
||||
{
|
||||
_api = api;
|
||||
}
|
||||
|
||||
public async Task Execute(IServiceConfiguration config, MediaNamingDto serviceDto)
|
||||
{
|
||||
await _api.UpdateNaming(config, serviceDto);
|
||||
}
|
||||
}
|
@ -0,0 +1,126 @@
|
||||
using Recyclarr.Compatibility.Sonarr;
|
||||
using Recyclarr.Config.Models;
|
||||
using Recyclarr.ServarrApi.MediaNaming;
|
||||
using Recyclarr.TrashGuide.MediaNaming;
|
||||
|
||||
namespace Recyclarr.Cli.Pipelines.MediaNaming.PipelinePhases;
|
||||
|
||||
public record InvalidNamingConfig(string Type, string ConfigValue);
|
||||
|
||||
public record ProcessedNamingConfig
|
||||
{
|
||||
public required MediaNamingDto Dto { get; init; }
|
||||
public IReadOnlyCollection<InvalidNamingConfig> InvalidNaming { get; init; } = new List<InvalidNamingConfig>();
|
||||
}
|
||||
|
||||
public class MediaNamingConfigPhase
|
||||
{
|
||||
private readonly IMediaNamingGuideService _guide;
|
||||
private readonly ISonarrCapabilityFetcher _sonarrCapabilities;
|
||||
private List<InvalidNamingConfig> _errors = new();
|
||||
|
||||
public MediaNamingConfigPhase(IMediaNamingGuideService guide, ISonarrCapabilityFetcher sonarrCapabilities)
|
||||
{
|
||||
_guide = guide;
|
||||
_sonarrCapabilities = sonarrCapabilities;
|
||||
}
|
||||
|
||||
public async Task<ProcessedNamingConfig> Execute(IServiceConfiguration config)
|
||||
{
|
||||
_errors = new List<InvalidNamingConfig>();
|
||||
|
||||
var dto = config switch
|
||||
{
|
||||
RadarrConfiguration c => ProcessRadarrNaming(c),
|
||||
SonarrConfiguration c => await ProcessSonarrNaming(c),
|
||||
_ => throw new ArgumentException("Configuration type unsupported for naming sync")
|
||||
};
|
||||
|
||||
return new ProcessedNamingConfig {Dto = dto, InvalidNaming = _errors};
|
||||
}
|
||||
|
||||
private MediaNamingDto ProcessRadarrNaming(RadarrConfiguration config)
|
||||
{
|
||||
var guideData = _guide.GetRadarrNamingData();
|
||||
var configData = config.MediaNaming;
|
||||
|
||||
return new RadarrMediaNamingDto
|
||||
{
|
||||
StandardMovieFormat = ObtainFormat(guideData.File, configData.Movie?.Format, "Movie File Format"),
|
||||
MovieFolderFormat = ObtainFormat(guideData.Folder, configData.Folder, "Movie Folder Format"),
|
||||
RenameMovies = configData.Movie?.Rename
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<MediaNamingDto> ProcessSonarrNaming(SonarrConfiguration config)
|
||||
{
|
||||
var guideData = _guide.GetSonarrNamingData();
|
||||
var configData = config.MediaNaming;
|
||||
var capabilities = await _sonarrCapabilities.GetCapabilities(config);
|
||||
var keySuffix = capabilities.SupportsCustomFormats ? ":4" : ":3";
|
||||
|
||||
return new SonarrMediaNamingDto
|
||||
{
|
||||
SeasonFolderFormat = ObtainFormat(guideData.Season, configData.Season, "Season Folder Format"),
|
||||
SeriesFolderFormat = ObtainFormat(guideData.Series, configData.Series, "Series Folder Format"),
|
||||
StandardEpisodeFormat = ObtainFormat(
|
||||
guideData.Episodes.Standard,
|
||||
configData.Episodes?.Standard,
|
||||
keySuffix,
|
||||
"Standard Episode Format"),
|
||||
DailyEpisodeFormat = ObtainFormat(
|
||||
guideData.Episodes.Daily,
|
||||
configData.Episodes?.Daily,
|
||||
keySuffix,
|
||||
"Daily Episode Format"),
|
||||
AnimeEpisodeFormat = ObtainFormat(
|
||||
guideData.Episodes.Anime,
|
||||
configData.Episodes?.Anime,
|
||||
keySuffix,
|
||||
"Anime Episode Format"),
|
||||
RenameEpisodes = configData.Episodes?.Rename
|
||||
};
|
||||
}
|
||||
|
||||
private string? ObtainFormat(
|
||||
IReadOnlyDictionary<string, string> guideFormats,
|
||||
string? configFormatKey,
|
||||
string errorDescription)
|
||||
{
|
||||
return ObtainFormat(guideFormats, configFormatKey, null, errorDescription);
|
||||
}
|
||||
|
||||
private string? ObtainFormat(
|
||||
IReadOnlyDictionary<string, string> guideFormats,
|
||||
string? configFormatKey,
|
||||
string? keySuffix,
|
||||
string errorDescription)
|
||||
{
|
||||
if (configFormatKey is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Use lower-case for the config value because System.Text.Json doesn't let us create a case-insensitive
|
||||
// dictionary. The MediaNamingGuideService converts all parsed guide JSON keys to lower case.
|
||||
var lowerKey = configFormatKey.ToLowerInvariant();
|
||||
|
||||
var keys = new List<string> {lowerKey};
|
||||
if (keySuffix is not null)
|
||||
{
|
||||
// Put the more specific key first
|
||||
keys.Insert(0, lowerKey + keySuffix);
|
||||
}
|
||||
|
||||
foreach (var k in keys)
|
||||
{
|
||||
if (guideFormats.TryGetValue(k, out var format))
|
||||
{
|
||||
return format;
|
||||
}
|
||||
}
|
||||
|
||||
_errors.Add(new InvalidNamingConfig(errorDescription, configFormatKey));
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
using Recyclarr.ServarrApi.MediaNaming;
|
||||
|
||||
namespace Recyclarr.Cli.Pipelines.MediaNaming.PipelinePhases;
|
||||
|
||||
public class MediaNamingPhaseLogger
|
||||
{
|
||||
private readonly ILogger _log;
|
||||
|
||||
public MediaNamingPhaseLogger(ILogger log)
|
||||
{
|
||||
_log = log;
|
||||
}
|
||||
|
||||
// Returning 'true' means to exit. 'false' means to proceed.
|
||||
public bool LogConfigPhaseAndExitIfNeeded(ProcessedNamingConfig config)
|
||||
{
|
||||
if (config.InvalidNaming.Any())
|
||||
{
|
||||
foreach (var (topic, invalidValue) in config.InvalidNaming)
|
||||
{
|
||||
_log.Error("An invalid media naming format is specified for {Topic}: {Value}", topic, invalidValue);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
var differences = config.Dto switch
|
||||
{
|
||||
RadarrMediaNamingDto x => x.GetDifferences(new RadarrMediaNamingDto()),
|
||||
SonarrMediaNamingDto x => x.GetDifferences(new SonarrMediaNamingDto()),
|
||||
_ => throw new ArgumentException("Unsupported configuration type in LogConfigPhase method")
|
||||
};
|
||||
|
||||
if (!differences.Any())
|
||||
{
|
||||
_log.Debug("No media naming changes to process");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void LogPersistenceResults(MediaNamingDto oldDto, MediaNamingDto newDto)
|
||||
{
|
||||
var differences = oldDto switch
|
||||
{
|
||||
RadarrMediaNamingDto x => x.GetDifferences(newDto),
|
||||
SonarrMediaNamingDto x => x.GetDifferences(newDto),
|
||||
_ => throw new ArgumentException("Unsupported configuration type in LogPersistenceResults method")
|
||||
};
|
||||
|
||||
if (differences.Any())
|
||||
{
|
||||
_log.Information("Media naming has been updated");
|
||||
_log.Debug("Naming differences: {Diff}", differences);
|
||||
}
|
||||
else
|
||||
{
|
||||
_log.Information("Media naming is up to date!");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
using Recyclarr.ServarrApi.MediaNaming;
|
||||
using Spectre.Console;
|
||||
|
||||
namespace Recyclarr.Cli.Pipelines.MediaNaming.PipelinePhases;
|
||||
|
||||
public class MediaNamingPreviewPhase
|
||||
{
|
||||
private readonly IAnsiConsole _console;
|
||||
private Table? _table;
|
||||
|
||||
public MediaNamingPreviewPhase(IAnsiConsole console)
|
||||
{
|
||||
_console = console;
|
||||
}
|
||||
|
||||
public void Execute(MediaNamingDto serviceDto)
|
||||
{
|
||||
_table = new Table()
|
||||
.Title("Media Naming [red](Preview)[/]")
|
||||
.AddColumns("[b]Field[/]", "[b]Value[/]");
|
||||
|
||||
switch (serviceDto)
|
||||
{
|
||||
case RadarrMediaNamingDto dto:
|
||||
PreviewRadarr(dto);
|
||||
break;
|
||||
|
||||
case SonarrMediaNamingDto dto:
|
||||
PreviewSonarr(dto);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ArgumentException("Config type not supported in media naming preview");
|
||||
}
|
||||
|
||||
_console.WriteLine();
|
||||
_console.Write(_table);
|
||||
}
|
||||
|
||||
private void AddRow(string field, object? value)
|
||||
{
|
||||
_table?.AddRow(field, value?.ToString() ?? "UNSET");
|
||||
}
|
||||
|
||||
private void PreviewRadarr(RadarrMediaNamingDto dto)
|
||||
{
|
||||
AddRow("Enable Movie Renames?", dto.RenameMovies);
|
||||
AddRow("Movie", dto.StandardMovieFormat);
|
||||
AddRow("Folder", dto.MovieFolderFormat);
|
||||
}
|
||||
|
||||
private void PreviewSonarr(SonarrMediaNamingDto dto)
|
||||
{
|
||||
AddRow("Enable Episode Renames?", dto.RenameEpisodes);
|
||||
AddRow("Series Folder", dto.SeriesFolderFormat);
|
||||
AddRow("Season Folder", dto.SeasonFolderFormat);
|
||||
AddRow("Standard Episodes", dto.StandardEpisodeFormat);
|
||||
AddRow("Daily Episodes", dto.DailyEpisodeFormat);
|
||||
AddRow("Anime Episodes", dto.AnimeEpisodeFormat);
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Recyclarr.ServarrApi.MediaNaming;
|
||||
|
||||
namespace Recyclarr.Cli.Pipelines.MediaNaming.PipelinePhases;
|
||||
|
||||
public class MediaNamingTransactionPhase
|
||||
{
|
||||
[SuppressMessage("Performance", "CA1822:Mark members as static")]
|
||||
public MediaNamingDto Execute(MediaNamingDto serviceData, ProcessedNamingConfig config)
|
||||
{
|
||||
return serviceData switch
|
||||
{
|
||||
RadarrMediaNamingDto dto => UpdateRadarrDto(dto, config),
|
||||
SonarrMediaNamingDto dto => UpdateSonarrDto(dto, config),
|
||||
_ => throw new ArgumentException("Config type not supported in media naming transation phase")
|
||||
};
|
||||
}
|
||||
|
||||
private static RadarrMediaNamingDto UpdateRadarrDto(RadarrMediaNamingDto serviceDto, ProcessedNamingConfig config)
|
||||
{
|
||||
var configDto = (RadarrMediaNamingDto) config.Dto;
|
||||
var combinedDto = serviceDto with
|
||||
{
|
||||
RenameMovies = configDto.RenameMovies,
|
||||
MovieFolderFormat = configDto.MovieFolderFormat
|
||||
};
|
||||
|
||||
if (configDto.RenameMovies is true)
|
||||
{
|
||||
combinedDto.StandardMovieFormat = configDto.StandardMovieFormat;
|
||||
}
|
||||
|
||||
return combinedDto;
|
||||
}
|
||||
|
||||
private static SonarrMediaNamingDto UpdateSonarrDto(SonarrMediaNamingDto serviceDto, ProcessedNamingConfig config)
|
||||
{
|
||||
var configDto = (SonarrMediaNamingDto) config.Dto;
|
||||
var combinedDto = serviceDto with
|
||||
{
|
||||
RenameEpisodes = configDto.RenameEpisodes,
|
||||
SeriesFolderFormat = configDto.SeriesFolderFormat,
|
||||
SeasonFolderFormat = configDto.SeasonFolderFormat
|
||||
};
|
||||
|
||||
if (configDto.RenameEpisodes is true)
|
||||
{
|
||||
combinedDto.StandardEpisodeFormat = configDto.StandardEpisodeFormat;
|
||||
combinedDto.DailyEpisodeFormat = configDto.DailyEpisodeFormat;
|
||||
combinedDto.AnimeEpisodeFormat = configDto.AnimeEpisodeFormat;
|
||||
}
|
||||
|
||||
return combinedDto;
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Recyclarr.Config.Parsing;
|
||||
|
||||
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
|
||||
public record RadarrMovieNamingConfigYaml
|
||||
{
|
||||
public bool? Rename { get; init; }
|
||||
public string? Format { get; init; }
|
||||
}
|
||||
|
||||
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
|
||||
public record RadarrMediaNamingConfigYaml
|
||||
{
|
||||
public string? Folder { get; init; }
|
||||
public RadarrMovieNamingConfigYaml? Movie { get; init; }
|
||||
}
|
||||
|
||||
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
|
||||
public record RadarrConfigYaml : ServiceConfigYaml
|
||||
{
|
||||
public RadarrMediaNamingConfigYaml? MediaNaming { get; init; }
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Recyclarr.Config.Parsing;
|
||||
|
||||
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
|
||||
public record ReleaseProfileFilterConfigYaml
|
||||
{
|
||||
public IReadOnlyCollection<string>? Include { get; init; }
|
||||
public IReadOnlyCollection<string>? Exclude { get; init; }
|
||||
}
|
||||
|
||||
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
|
||||
public record ReleaseProfileConfigYaml
|
||||
{
|
||||
public IReadOnlyCollection<string>? TrashIds { get; init; }
|
||||
public bool StrictNegativeScores { get; init; }
|
||||
public IReadOnlyCollection<string>? Tags { get; init; }
|
||||
public ReleaseProfileFilterConfigYaml? Filter { get; init; }
|
||||
}
|
||||
|
||||
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
|
||||
public record SonarrEpisodeNamingConfigYaml
|
||||
{
|
||||
public bool? Rename { get; init; }
|
||||
public string? Standard { get; init; }
|
||||
public string? Daily { get; init; }
|
||||
public string? Anime { get; init; }
|
||||
}
|
||||
|
||||
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
|
||||
public record SonarrMediaNamingConfigYaml
|
||||
{
|
||||
public string? Season { get; init; }
|
||||
public string? Series { get; init; }
|
||||
public SonarrEpisodeNamingConfigYaml? Episodes { get; init; }
|
||||
}
|
||||
|
||||
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
|
||||
public record SonarrConfigYaml : ServiceConfigYaml
|
||||
{
|
||||
public IReadOnlyCollection<ReleaseProfileConfigYaml>? ReleaseProfiles { get; init; }
|
||||
public SonarrMediaNamingConfigYaml? MediaNaming { get; init; }
|
||||
}
|
@ -1,5 +1,30 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Recyclarr.Config.Parsing.PostProcessing.ConfigMerging;
|
||||
|
||||
public class RadarrConfigMerger : ServiceConfigMerger<RadarrConfigYaml>
|
||||
{
|
||||
public override RadarrConfigYaml Merge(RadarrConfigYaml a, RadarrConfigYaml b)
|
||||
{
|
||||
return base.Merge(a, b) with
|
||||
{
|
||||
MediaNaming = Combine(a.MediaNaming, b.MediaNaming, MergeMediaNaming)
|
||||
};
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "WithExpressionModifiesAllMembers")]
|
||||
private static RadarrMediaNamingConfigYaml MergeMediaNaming(
|
||||
RadarrMediaNamingConfigYaml a,
|
||||
RadarrMediaNamingConfigYaml b)
|
||||
{
|
||||
return a with
|
||||
{
|
||||
Folder = b.Folder ?? a.Folder,
|
||||
Movie = Combine(a.Movie, b.Movie, (a1, b1) => a1 with
|
||||
{
|
||||
Rename = b1.Rename ?? a1.Rename,
|
||||
Format = b1.Format ?? a1.Format
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,9 @@
|
||||
using Recyclarr.Config.Models;
|
||||
|
||||
namespace Recyclarr.ServarrApi.MediaNaming;
|
||||
|
||||
public interface IMediaNamingApiService
|
||||
{
|
||||
Task<MediaNamingDto> GetNaming(IServiceConfiguration config);
|
||||
Task UpdateNaming(IServiceConfiguration config, MediaNamingDto dto);
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
using Flurl.Http;
|
||||
using Recyclarr.Common;
|
||||
using Recyclarr.Config.Models;
|
||||
using Recyclarr.ServarrApi.Http;
|
||||
|
||||
namespace Recyclarr.ServarrApi.MediaNaming;
|
||||
|
||||
public class MediaNamingApiService : IMediaNamingApiService
|
||||
{
|
||||
private readonly IServiceRequestBuilder _service;
|
||||
|
||||
public MediaNamingApiService(IServiceRequestBuilder service)
|
||||
{
|
||||
_service = service;
|
||||
}
|
||||
|
||||
public async Task<MediaNamingDto> GetNaming(IServiceConfiguration config)
|
||||
{
|
||||
var response = await _service.Request(config, "config", "naming")
|
||||
.GetAsync();
|
||||
|
||||
return config.ServiceType switch
|
||||
{
|
||||
SupportedServices.Radarr => await response.GetJsonAsync<RadarrMediaNamingDto>(),
|
||||
SupportedServices.Sonarr => await response.GetJsonAsync<SonarrMediaNamingDto>(),
|
||||
_ => throw new ArgumentException("Configuration type unsupported in GetNaming() API")
|
||||
};
|
||||
}
|
||||
|
||||
public async Task UpdateNaming(IServiceConfiguration config, MediaNamingDto dto)
|
||||
{
|
||||
await _service.Request(config, "config", "naming")
|
||||
.PutJsonAsync(dto);
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Recyclarr.ServarrApi.MediaNaming;
|
||||
|
||||
[SuppressMessage("SonarLint", "S2094")]
|
||||
public abstract record MediaNamingDto;
|
@ -0,0 +1,73 @@
|
||||
namespace Recyclarr.ServarrApi.MediaNaming;
|
||||
|
||||
public static class MediaNamingDtoExtensions
|
||||
{
|
||||
public static IReadOnlyCollection<string> GetDifferences(this RadarrMediaNamingDto left, MediaNamingDto other)
|
||||
{
|
||||
var diff = new List<string>();
|
||||
|
||||
if (other is not RadarrMediaNamingDto right)
|
||||
{
|
||||
throw new ArgumentException("'other' is of the wrong type");
|
||||
}
|
||||
|
||||
if (left.RenameMovies != right.RenameMovies)
|
||||
{
|
||||
diff.Add(nameof(left.RenameMovies));
|
||||
}
|
||||
|
||||
if (left.MovieFolderFormat != right.MovieFolderFormat)
|
||||
{
|
||||
diff.Add(nameof(left.MovieFolderFormat));
|
||||
}
|
||||
|
||||
if (left.StandardMovieFormat != right.StandardMovieFormat)
|
||||
{
|
||||
diff.Add(nameof(left.StandardMovieFormat));
|
||||
}
|
||||
|
||||
return diff;
|
||||
}
|
||||
|
||||
public static IReadOnlyCollection<string> GetDifferences(this SonarrMediaNamingDto left, MediaNamingDto other)
|
||||
{
|
||||
var diff = new List<string>();
|
||||
|
||||
if (other is not SonarrMediaNamingDto right)
|
||||
{
|
||||
throw new ArgumentException("'other' is of the wrong type");
|
||||
}
|
||||
|
||||
if (left.RenameEpisodes != right.RenameEpisodes)
|
||||
{
|
||||
diff.Add(nameof(left.RenameEpisodes));
|
||||
}
|
||||
|
||||
if (left.SeasonFolderFormat != right.SeasonFolderFormat)
|
||||
{
|
||||
diff.Add(nameof(left.SeasonFolderFormat));
|
||||
}
|
||||
|
||||
if (left.SeriesFolderFormat != right.SeriesFolderFormat)
|
||||
{
|
||||
diff.Add(nameof(left.SeriesFolderFormat));
|
||||
}
|
||||
|
||||
if (left.StandardEpisodeFormat != right.StandardEpisodeFormat)
|
||||
{
|
||||
diff.Add(nameof(left.StandardEpisodeFormat));
|
||||
}
|
||||
|
||||
if (left.DailyEpisodeFormat != right.DailyEpisodeFormat)
|
||||
{
|
||||
diff.Add(nameof(left.DailyEpisodeFormat));
|
||||
}
|
||||
|
||||
if (left.AnimeEpisodeFormat != right.AnimeEpisodeFormat)
|
||||
{
|
||||
diff.Add(nameof(left.AnimeEpisodeFormat));
|
||||
}
|
||||
|
||||
return diff;
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Recyclarr.ServarrApi.MediaNaming;
|
||||
|
||||
public record RadarrMediaNamingDto : MediaNamingDto
|
||||
{
|
||||
private string? _movieFormat;
|
||||
private readonly string? _folderFormat;
|
||||
private readonly bool? _renameMovies;
|
||||
|
||||
public string? StandardMovieFormat
|
||||
{
|
||||
get => _movieFormat;
|
||||
set => DtoUtil.SetIfNotNull(ref _movieFormat, value);
|
||||
}
|
||||
|
||||
public string? MovieFolderFormat
|
||||
{
|
||||
get => _folderFormat;
|
||||
init => DtoUtil.SetIfNotNull(ref _folderFormat, value);
|
||||
}
|
||||
|
||||
public bool? RenameMovies
|
||||
{
|
||||
get => _renameMovies;
|
||||
init => DtoUtil.SetIfNotNull(ref _renameMovies, value);
|
||||
}
|
||||
|
||||
[UsedImplicitly, JsonExtensionData]
|
||||
public Dictionary<string, object> ExtraJson { get; init; } = new();
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Recyclarr.ServarrApi.MediaNaming;
|
||||
|
||||
public record SonarrMediaNamingDto : MediaNamingDto
|
||||
{
|
||||
private readonly string? _seriesFolderFormat;
|
||||
private readonly string? _seasonFolderFormat;
|
||||
private string? _standardEpisodeFormat;
|
||||
private string? _dailyEpisodeFormat;
|
||||
private string? _animeEpisodeFormat;
|
||||
private readonly bool? _renameEpisodes;
|
||||
|
||||
public string? SeriesFolderFormat
|
||||
{
|
||||
get => _seriesFolderFormat;
|
||||
init => DtoUtil.SetIfNotNull(ref _seriesFolderFormat, value);
|
||||
}
|
||||
|
||||
public string? SeasonFolderFormat
|
||||
{
|
||||
get => _seasonFolderFormat;
|
||||
init => DtoUtil.SetIfNotNull(ref _seasonFolderFormat, value);
|
||||
}
|
||||
|
||||
public string? StandardEpisodeFormat
|
||||
{
|
||||
get => _standardEpisodeFormat;
|
||||
set => DtoUtil.SetIfNotNull(ref _standardEpisodeFormat, value);
|
||||
}
|
||||
|
||||
public string? DailyEpisodeFormat
|
||||
{
|
||||
get => _dailyEpisodeFormat;
|
||||
set => DtoUtil.SetIfNotNull(ref _dailyEpisodeFormat, value);
|
||||
}
|
||||
|
||||
public string? AnimeEpisodeFormat
|
||||
{
|
||||
get => _animeEpisodeFormat;
|
||||
set => DtoUtil.SetIfNotNull(ref _animeEpisodeFormat, value);
|
||||
}
|
||||
|
||||
public bool? RenameEpisodes
|
||||
{
|
||||
get => _renameEpisodes;
|
||||
init => DtoUtil.SetIfNotNull(ref _renameEpisodes, value);
|
||||
}
|
||||
|
||||
[UsedImplicitly, JsonExtensionData]
|
||||
public Dictionary<string, object> ExtraJson { get; init; } = new();
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
namespace Recyclarr.TrashGuide.MediaNaming;
|
||||
|
||||
public interface IMediaNamingGuideService
|
||||
{
|
||||
RadarrMediaNamingData GetRadarrNamingData();
|
||||
SonarrMediaNamingData GetSonarrNamingData();
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
using System.IO.Abstractions;
|
||||
using Recyclarr.Common;
|
||||
using Recyclarr.Json.Loading;
|
||||
using Recyclarr.Repo;
|
||||
|
||||
namespace Recyclarr.TrashGuide.MediaNaming;
|
||||
|
||||
public class MediaNamingGuideService : IMediaNamingGuideService
|
||||
{
|
||||
private readonly IRepoMetadataBuilder _metadataBuilder;
|
||||
private readonly GuideJsonLoader _jsonLoader;
|
||||
|
||||
public MediaNamingGuideService(IRepoMetadataBuilder metadataBuilder, GuideJsonLoader jsonLoader)
|
||||
{
|
||||
_metadataBuilder = metadataBuilder;
|
||||
_jsonLoader = jsonLoader;
|
||||
}
|
||||
|
||||
private IReadOnlyList<IDirectoryInfo> CreatePaths(SupportedServices serviceType)
|
||||
{
|
||||
var metadata = _metadataBuilder.GetMetadata();
|
||||
return serviceType switch
|
||||
{
|
||||
SupportedServices.Radarr => _metadataBuilder.ToDirectoryInfoList(metadata.JsonPaths.Radarr.Naming),
|
||||
SupportedServices.Sonarr => _metadataBuilder.ToDirectoryInfoList(metadata.JsonPaths.Sonarr.Naming),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(serviceType), serviceType, null)
|
||||
};
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<string, string> JoinDictionaries(
|
||||
IEnumerable<IReadOnlyDictionary<string, string>> dictionaries)
|
||||
{
|
||||
return dictionaries
|
||||
.SelectMany(x => x.Select(y => (y.Key, y.Value)))
|
||||
.ToDictionary(x => x.Key.ToLowerInvariant(), x => x.Value);
|
||||
}
|
||||
|
||||
public RadarrMediaNamingData GetRadarrNamingData()
|
||||
{
|
||||
var paths = CreatePaths(SupportedServices.Radarr);
|
||||
var data = _jsonLoader.LoadAllFilesAtPaths<RadarrMediaNamingData>(paths);
|
||||
return new RadarrMediaNamingData
|
||||
{
|
||||
File = JoinDictionaries(data.Select(x => x.File)),
|
||||
Folder = JoinDictionaries(data.Select(x => x.Folder))
|
||||
};
|
||||
}
|
||||
|
||||
public SonarrMediaNamingData GetSonarrNamingData()
|
||||
{
|
||||
var paths = CreatePaths(SupportedServices.Sonarr);
|
||||
var data = _jsonLoader.LoadAllFilesAtPaths<SonarrMediaNamingData>(paths);
|
||||
return new SonarrMediaNamingData
|
||||
{
|
||||
Season = JoinDictionaries(data.Select(x => x.Season)),
|
||||
Series = JoinDictionaries(data.Select(x => x.Series)),
|
||||
Episodes = new SonarrEpisodeNamingData
|
||||
{
|
||||
Anime = JoinDictionaries(data.Select(x => x.Episodes.Anime)),
|
||||
Daily = JoinDictionaries(data.Select(x => x.Episodes.Daily)),
|
||||
Standard = JoinDictionaries(data.Select(x => x.Episodes.Standard))
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
namespace Recyclarr.TrashGuide.MediaNaming;
|
||||
|
||||
public record RadarrMediaNamingData
|
||||
{
|
||||
public IReadOnlyDictionary<string, string> Folder { get; init; } = new Dictionary<string, string>();
|
||||
public IReadOnlyDictionary<string, string> File { get; init; } = new Dictionary<string, string>();
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
namespace Recyclarr.TrashGuide.MediaNaming;
|
||||
|
||||
public record SonarrEpisodeNamingData
|
||||
{
|
||||
public IReadOnlyDictionary<string, string> Standard { get; init; } = new Dictionary<string, string>();
|
||||
public IReadOnlyDictionary<string, string> Daily { get; init; } = new Dictionary<string, string>();
|
||||
public IReadOnlyDictionary<string, string> Anime { get; init; } = new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
public record SonarrMediaNamingData
|
||||
{
|
||||
public IReadOnlyDictionary<string, string> Season { get; init; } = new Dictionary<string, string>();
|
||||
public IReadOnlyDictionary<string, string> Series { get; init; } = new Dictionary<string, string>();
|
||||
public SonarrEpisodeNamingData Episodes { get; init; } = new();
|
||||
}
|
@ -0,0 +1,282 @@
|
||||
using Recyclarr.Cli.Pipelines.MediaNaming.PipelinePhases;
|
||||
using Recyclarr.Common;
|
||||
using Recyclarr.Compatibility.Sonarr;
|
||||
using Recyclarr.Config.Models;
|
||||
using Recyclarr.ServarrApi.MediaNaming;
|
||||
using Recyclarr.TrashGuide.MediaNaming;
|
||||
|
||||
namespace Recyclarr.Cli.Tests.Pipelines.MediaNaming;
|
||||
|
||||
[TestFixture]
|
||||
[Parallelizable(ParallelScope.All)]
|
||||
public class MediaNamingConfigPhaseTest
|
||||
{
|
||||
private static readonly SonarrMediaNamingData SonarrNamingData = new()
|
||||
{
|
||||
Season = new Dictionary<string, string>
|
||||
{
|
||||
{"default", "season_default"}
|
||||
},
|
||||
Series = new Dictionary<string, string>
|
||||
{
|
||||
{"default", "series_default"},
|
||||
{"plex", "series_plex"},
|
||||
{"emby", "series_emby"}
|
||||
},
|
||||
Episodes = new SonarrEpisodeNamingData
|
||||
{
|
||||
Standard = new Dictionary<string, string>
|
||||
{
|
||||
{"default:3", "episodes_standard_default_3"},
|
||||
{"default:4", "episodes_standard_default_4"},
|
||||
{"original", "episodes_standard_original"}
|
||||
},
|
||||
Daily = new Dictionary<string, string>
|
||||
{
|
||||
{"default:3", "episodes_daily_default_3"},
|
||||
{"default:4", "episodes_daily_default_4"},
|
||||
{"original", "episodes_daily_original"}
|
||||
},
|
||||
Anime = new Dictionary<string, string>
|
||||
{
|
||||
{"default:3", "episodes_anime_default_3"},
|
||||
{"default:4", "episodes_anime_default_4"}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private static readonly RadarrMediaNamingData RadarrNamingData = new()
|
||||
{
|
||||
Folder = new Dictionary<string, string>
|
||||
{
|
||||
{"default", "folder_default"},
|
||||
{"plex", "folder_plex"},
|
||||
{"emby", "folder_emby"}
|
||||
},
|
||||
File = new Dictionary<string, string>
|
||||
{
|
||||
{"default", "file_default"},
|
||||
{"emby", "file_emby"},
|
||||
{"jellyfin", "file_jellyfin"}
|
||||
}
|
||||
};
|
||||
|
||||
[Test, AutoMockData]
|
||||
public async Task Sonarr_v3_naming(
|
||||
[Frozen] ISonarrCapabilityFetcher capabilities,
|
||||
[Frozen] IMediaNamingGuideService guide,
|
||||
MediaNamingConfigPhase sut)
|
||||
{
|
||||
capabilities.GetCapabilities(default!).ReturnsForAnyArgs(new SonarrCapabilities
|
||||
{
|
||||
SupportsCustomFormats = false
|
||||
});
|
||||
|
||||
guide.GetSonarrNamingData().Returns(SonarrNamingData);
|
||||
|
||||
var config = new SonarrConfiguration
|
||||
{
|
||||
InstanceName = "sonarr",
|
||||
MediaNaming = new SonarrMediaNamingConfig
|
||||
{
|
||||
Season = "default",
|
||||
Series = "plex",
|
||||
Episodes = new SonarrEpisodeNamingConfig
|
||||
{
|
||||
Rename = true,
|
||||
Standard = "default",
|
||||
Daily = "default",
|
||||
Anime = "default"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var result = await sut.Execute(config);
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result.Should().BeEquivalentTo(new ProcessedNamingConfig
|
||||
{
|
||||
Dto = new SonarrMediaNamingDto
|
||||
{
|
||||
RenameEpisodes = true,
|
||||
SeasonFolderFormat = "season_default",
|
||||
SeriesFolderFormat = "series_plex",
|
||||
StandardEpisodeFormat = "episodes_standard_default_3",
|
||||
DailyEpisodeFormat = "episodes_daily_default_3",
|
||||
AnimeEpisodeFormat = "episodes_anime_default_3"
|
||||
}
|
||||
},
|
||||
o => o.RespectingRuntimeTypes());
|
||||
}
|
||||
|
||||
[Test, AutoMockData]
|
||||
public async Task Sonarr_v4_naming(
|
||||
[Frozen] ISonarrCapabilityFetcher capabilities,
|
||||
[Frozen] IMediaNamingGuideService guide,
|
||||
MediaNamingConfigPhase sut)
|
||||
{
|
||||
capabilities.GetCapabilities(default!).ReturnsForAnyArgs(new SonarrCapabilities
|
||||
{
|
||||
SupportsCustomFormats = true
|
||||
});
|
||||
|
||||
guide.GetSonarrNamingData().Returns(SonarrNamingData);
|
||||
|
||||
var config = new SonarrConfiguration
|
||||
{
|
||||
InstanceName = "sonarr",
|
||||
MediaNaming = new SonarrMediaNamingConfig
|
||||
{
|
||||
Season = "default",
|
||||
Series = "plex",
|
||||
Episodes = new SonarrEpisodeNamingConfig
|
||||
{
|
||||
Rename = true,
|
||||
Standard = "default",
|
||||
Daily = "default",
|
||||
Anime = "default"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var result = await sut.Execute(config);
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result.Should().BeEquivalentTo(new ProcessedNamingConfig
|
||||
{
|
||||
Dto = new SonarrMediaNamingDto
|
||||
{
|
||||
RenameEpisodes = true,
|
||||
SeasonFolderFormat = "season_default",
|
||||
SeriesFolderFormat = "series_plex",
|
||||
StandardEpisodeFormat = "episodes_standard_default_4",
|
||||
DailyEpisodeFormat = "episodes_daily_default_4",
|
||||
AnimeEpisodeFormat = "episodes_anime_default_4"
|
||||
}
|
||||
},
|
||||
o => o.RespectingRuntimeTypes());
|
||||
}
|
||||
|
||||
[Test, AutoMockData]
|
||||
public async Task Sonarr_invalid_names(
|
||||
[Frozen] ISonarrCapabilityFetcher capabilities,
|
||||
[Frozen] IMediaNamingGuideService guide,
|
||||
MediaNamingConfigPhase sut)
|
||||
{
|
||||
capabilities.GetCapabilities(default!).ReturnsForAnyArgs(new SonarrCapabilities
|
||||
{
|
||||
SupportsCustomFormats = true
|
||||
});
|
||||
|
||||
guide.GetSonarrNamingData().Returns(SonarrNamingData);
|
||||
|
||||
var config = new SonarrConfiguration
|
||||
{
|
||||
InstanceName = "sonarr",
|
||||
MediaNaming = new SonarrMediaNamingConfig
|
||||
{
|
||||
Season = "bad1",
|
||||
Series = "bad2",
|
||||
Episodes = new SonarrEpisodeNamingConfig
|
||||
{
|
||||
Rename = true,
|
||||
Standard = "bad3",
|
||||
Daily = "bad4",
|
||||
Anime = "bad5"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var result = await sut.Execute(config);
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result.Should().BeEquivalentTo(new ProcessedNamingConfig
|
||||
{
|
||||
Dto = new SonarrMediaNamingDto
|
||||
{
|
||||
RenameEpisodes = true
|
||||
},
|
||||
InvalidNaming = new[]
|
||||
{
|
||||
new InvalidNamingConfig("Season Folder Format", "bad1"),
|
||||
new InvalidNamingConfig("Series Folder Format", "bad2"),
|
||||
new InvalidNamingConfig("Standard Episode Format", "bad3"),
|
||||
new InvalidNamingConfig("Daily Episode Format", "bad4"),
|
||||
new InvalidNamingConfig("Anime Episode Format", "bad5")
|
||||
}
|
||||
},
|
||||
o => o.RespectingRuntimeTypes());
|
||||
}
|
||||
|
||||
[Test, AutoMockData]
|
||||
public async Task Radarr_naming(
|
||||
[Frozen] IMediaNamingGuideService guide,
|
||||
MediaNamingConfigPhase sut)
|
||||
{
|
||||
guide.GetRadarrNamingData().Returns(RadarrNamingData);
|
||||
|
||||
var config = new RadarrConfiguration
|
||||
{
|
||||
InstanceName = "radarr",
|
||||
MediaNaming = new RadarrMediaNamingConfig
|
||||
{
|
||||
Folder = "plex",
|
||||
Movie = new RadarrMovieNamingConfig
|
||||
{
|
||||
Rename = true,
|
||||
Format = "emby"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var result = await sut.Execute(config);
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result.Should().BeEquivalentTo(new ProcessedNamingConfig
|
||||
{
|
||||
Dto = new RadarrMediaNamingDto
|
||||
{
|
||||
RenameMovies = true,
|
||||
StandardMovieFormat = "file_emby",
|
||||
MovieFolderFormat = "folder_plex"
|
||||
}
|
||||
},
|
||||
o => o.RespectingRuntimeTypes());
|
||||
}
|
||||
|
||||
private sealed record UnsupportedConfigType : ServiceConfiguration
|
||||
{
|
||||
public override SupportedServices ServiceType => default!;
|
||||
}
|
||||
|
||||
[Test, AutoMockData]
|
||||
public async Task Throw_on_unknown_config_type(
|
||||
MediaNamingConfigPhase sut)
|
||||
{
|
||||
var act = () => sut.Execute(new UnsupportedConfigType {InstanceName = ""});
|
||||
await act.Should().ThrowAsync<ArgumentException>();
|
||||
}
|
||||
|
||||
[Test, AutoMockData]
|
||||
public async Task Assign_null_when_config_null(
|
||||
[Frozen] IMediaNamingGuideService guide,
|
||||
MediaNamingConfigPhase sut)
|
||||
{
|
||||
guide.GetRadarrNamingData().Returns(RadarrNamingData);
|
||||
|
||||
var config = new RadarrConfiguration
|
||||
{
|
||||
InstanceName = "",
|
||||
MediaNaming = new RadarrMediaNamingConfig
|
||||
{
|
||||
Folder = null
|
||||
}
|
||||
};
|
||||
|
||||
var result = await sut.Execute(config);
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result.Dto.Should().BeOfType<RadarrMediaNamingDto>()
|
||||
.Which.MovieFolderFormat.Should().BeNull();
|
||||
}
|
||||
}
|
@ -0,0 +1,109 @@
|
||||
using Recyclarr.Cli.Pipelines.MediaNaming.PipelinePhases;
|
||||
using Recyclarr.ServarrApi.MediaNaming;
|
||||
|
||||
namespace Recyclarr.Cli.Tests.Pipelines.MediaNaming;
|
||||
|
||||
[TestFixture]
|
||||
[Parallelizable(ParallelScope.All)]
|
||||
public class MediaNamingTransactionPhaseRadarrTest
|
||||
{
|
||||
[Test, AutoMockData]
|
||||
public void Radarr_left_null(
|
||||
MediaNamingTransactionPhase sut)
|
||||
{
|
||||
var left = new RadarrMediaNamingDto();
|
||||
|
||||
var right = new ProcessedNamingConfig
|
||||
{
|
||||
Dto = new RadarrMediaNamingDto
|
||||
{
|
||||
RenameMovies = true,
|
||||
StandardMovieFormat = "file_format",
|
||||
MovieFolderFormat = "folder_format"
|
||||
}
|
||||
};
|
||||
|
||||
var result = sut.Execute(left, right);
|
||||
|
||||
result.Should().BeEquivalentTo(right.Dto, o => o.RespectingRuntimeTypes());
|
||||
}
|
||||
|
||||
[Test, AutoMockData]
|
||||
public void Radarr_right_null(
|
||||
MediaNamingTransactionPhase sut)
|
||||
{
|
||||
var left = new RadarrMediaNamingDto
|
||||
{
|
||||
RenameMovies = true,
|
||||
StandardMovieFormat = "file_format",
|
||||
MovieFolderFormat = "folder_format"
|
||||
};
|
||||
|
||||
var right = new ProcessedNamingConfig
|
||||
{
|
||||
Dto = new RadarrMediaNamingDto()
|
||||
};
|
||||
|
||||
var result = sut.Execute(left, right);
|
||||
|
||||
result.Should().BeEquivalentTo(left, o => o.RespectingRuntimeTypes());
|
||||
}
|
||||
|
||||
[Test, AutoMockData]
|
||||
public void Radarr_right_and_left_with_rename(
|
||||
MediaNamingTransactionPhase sut)
|
||||
{
|
||||
var left = new RadarrMediaNamingDto
|
||||
{
|
||||
RenameMovies = false,
|
||||
StandardMovieFormat = "file_format",
|
||||
MovieFolderFormat = "folder_format"
|
||||
};
|
||||
|
||||
var right = new ProcessedNamingConfig
|
||||
{
|
||||
Dto = new RadarrMediaNamingDto
|
||||
{
|
||||
RenameMovies = true,
|
||||
StandardMovieFormat = "file_format2",
|
||||
MovieFolderFormat = "folder_format2"
|
||||
}
|
||||
};
|
||||
|
||||
var result = sut.Execute(left, right);
|
||||
|
||||
result.Should().BeEquivalentTo(right.Dto, o => o.RespectingRuntimeTypes());
|
||||
}
|
||||
|
||||
[Test, AutoMockData]
|
||||
public void Radarr_right_and_left_without_rename(
|
||||
MediaNamingTransactionPhase sut)
|
||||
{
|
||||
var left = new RadarrMediaNamingDto
|
||||
{
|
||||
RenameMovies = true,
|
||||
StandardMovieFormat = "file_format",
|
||||
MovieFolderFormat = "folder_format"
|
||||
};
|
||||
|
||||
var right = new ProcessedNamingConfig
|
||||
{
|
||||
Dto = new RadarrMediaNamingDto
|
||||
{
|
||||
RenameMovies = false,
|
||||
StandardMovieFormat = "file_format2",
|
||||
MovieFolderFormat = "folder_format2"
|
||||
}
|
||||
};
|
||||
|
||||
var result = sut.Execute(left, right);
|
||||
|
||||
result.Should().BeEquivalentTo(new RadarrMediaNamingDto
|
||||
{
|
||||
RenameMovies = false,
|
||||
StandardMovieFormat = "file_format",
|
||||
MovieFolderFormat = "folder_format2"
|
||||
},
|
||||
o => o.RespectingRuntimeTypes());
|
||||
}
|
||||
}
|
@ -0,0 +1,130 @@
|
||||
using Recyclarr.Cli.Pipelines.MediaNaming.PipelinePhases;
|
||||
using Recyclarr.ServarrApi.MediaNaming;
|
||||
|
||||
namespace Recyclarr.Cli.Tests.Pipelines.MediaNaming;
|
||||
|
||||
[TestFixture]
|
||||
[Parallelizable(ParallelScope.All)]
|
||||
public class MediaNamingTransactionPhaseSonarrTest
|
||||
{
|
||||
[Test, AutoMockData]
|
||||
public void Sonarr_left_null(
|
||||
MediaNamingTransactionPhase sut)
|
||||
{
|
||||
var left = new SonarrMediaNamingDto();
|
||||
|
||||
var right = new ProcessedNamingConfig
|
||||
{
|
||||
Dto = new SonarrMediaNamingDto
|
||||
{
|
||||
RenameEpisodes = true,
|
||||
SeasonFolderFormat = "season_default",
|
||||
SeriesFolderFormat = "series_plex",
|
||||
StandardEpisodeFormat = "episodes_standard_default_3",
|
||||
DailyEpisodeFormat = "episodes_daily_default_3",
|
||||
AnimeEpisodeFormat = "episodes_anime_default_3"
|
||||
}
|
||||
};
|
||||
|
||||
var result = sut.Execute(left, right);
|
||||
|
||||
result.Should().BeEquivalentTo(right.Dto, o => o.RespectingRuntimeTypes());
|
||||
}
|
||||
|
||||
[Test, AutoMockData]
|
||||
public void Sonarr_right_null(
|
||||
MediaNamingTransactionPhase sut)
|
||||
{
|
||||
var left = new SonarrMediaNamingDto
|
||||
{
|
||||
RenameEpisodes = true,
|
||||
SeasonFolderFormat = "season_default",
|
||||
SeriesFolderFormat = "series_plex",
|
||||
StandardEpisodeFormat = "episodes_standard_default_3",
|
||||
DailyEpisodeFormat = "episodes_daily_default_3",
|
||||
AnimeEpisodeFormat = "episodes_anime_default_3"
|
||||
};
|
||||
|
||||
var right = new ProcessedNamingConfig
|
||||
{
|
||||
Dto = new SonarrMediaNamingDto()
|
||||
};
|
||||
|
||||
var result = sut.Execute(left, right);
|
||||
|
||||
result.Should().BeEquivalentTo(left, o => o.RespectingRuntimeTypes());
|
||||
}
|
||||
|
||||
[Test, AutoMockData]
|
||||
public void Sonarr_right_and_left_with_rename(
|
||||
MediaNamingTransactionPhase sut)
|
||||
{
|
||||
var left = new SonarrMediaNamingDto
|
||||
{
|
||||
RenameEpisodes = false,
|
||||
SeasonFolderFormat = "season_default",
|
||||
SeriesFolderFormat = "series_plex",
|
||||
StandardEpisodeFormat = "episodes_standard_default",
|
||||
DailyEpisodeFormat = "episodes_daily_default",
|
||||
AnimeEpisodeFormat = "episodes_anime_default"
|
||||
};
|
||||
|
||||
var right = new ProcessedNamingConfig
|
||||
{
|
||||
Dto = new SonarrMediaNamingDto
|
||||
{
|
||||
RenameEpisodes = true,
|
||||
SeasonFolderFormat = "season_default2",
|
||||
SeriesFolderFormat = "series_plex2",
|
||||
StandardEpisodeFormat = "episodes_standard_default2",
|
||||
DailyEpisodeFormat = "episodes_daily_default2",
|
||||
AnimeEpisodeFormat = "episodes_anime_default2"
|
||||
}
|
||||
};
|
||||
|
||||
var result = sut.Execute(left, right);
|
||||
|
||||
result.Should().BeEquivalentTo(right.Dto, o => o.RespectingRuntimeTypes());
|
||||
}
|
||||
|
||||
[Test, AutoMockData]
|
||||
public void Sonarr_right_and_left_without_rename(
|
||||
MediaNamingTransactionPhase sut)
|
||||
{
|
||||
var left = new SonarrMediaNamingDto
|
||||
{
|
||||
RenameEpisodes = true,
|
||||
SeasonFolderFormat = "season_default",
|
||||
SeriesFolderFormat = "series_plex",
|
||||
StandardEpisodeFormat = "episodes_standard_default",
|
||||
DailyEpisodeFormat = "episodes_daily_default",
|
||||
AnimeEpisodeFormat = "episodes_anime_default"
|
||||
};
|
||||
|
||||
var right = new ProcessedNamingConfig
|
||||
{
|
||||
Dto = new SonarrMediaNamingDto
|
||||
{
|
||||
RenameEpisodes = false,
|
||||
SeasonFolderFormat = "season_default2",
|
||||
SeriesFolderFormat = "series_plex2",
|
||||
StandardEpisodeFormat = "episodes_standard_default2",
|
||||
DailyEpisodeFormat = "episodes_daily_default2",
|
||||
AnimeEpisodeFormat = "episodes_anime_default2"
|
||||
}
|
||||
};
|
||||
|
||||
var result = sut.Execute(left, right);
|
||||
|
||||
result.Should().BeEquivalentTo(new SonarrMediaNamingDto
|
||||
{
|
||||
RenameEpisodes = false,
|
||||
SeasonFolderFormat = "season_default2",
|
||||
SeriesFolderFormat = "series_plex2",
|
||||
StandardEpisodeFormat = "episodes_standard_default",
|
||||
DailyEpisodeFormat = "episodes_daily_default",
|
||||
AnimeEpisodeFormat = "episodes_anime_default"
|
||||
},
|
||||
o => o.RespectingRuntimeTypes());
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
{
|
||||
"folder": {
|
||||
"plex": "folder_plex"
|
||||
},
|
||||
"file": {
|
||||
"emby": "file_emby"
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
{
|
||||
"folder": {
|
||||
"default": "folder_default",
|
||||
"emby": "folder_emby"
|
||||
},
|
||||
"file": {
|
||||
"default": "file_default",
|
||||
"jellyfin": "file_jellyfin"
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
{
|
||||
"series": {
|
||||
"default": "series_default"
|
||||
},
|
||||
"episodes": {
|
||||
"standard": {
|
||||
"default:3": "episodes_standard_default_3",
|
||||
"original": "episodes_standard_original"
|
||||
},
|
||||
"daily": {
|
||||
"default:4": "episodes_daily_default_4",
|
||||
"original": "episodes_daily_original"
|
||||
},
|
||||
"anime": {
|
||||
"default:3": "episodes_anime_default_3",
|
||||
"default:4": "episodes_anime_default_4"
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
{
|
||||
"season": {
|
||||
"default": "season_default"
|
||||
},
|
||||
"series": {
|
||||
"plex": "series_plex",
|
||||
"emby": "series_emby"
|
||||
},
|
||||
"episodes": {
|
||||
"standard": {
|
||||
"default:4": "episodes_standard_default_4"
|
||||
},
|
||||
"daily": {
|
||||
"default:3": "episodes_daily_default_3"
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
using System.IO.Abstractions;
|
||||
using Recyclarr.Common.Extensions;
|
||||
using Recyclarr.Repo;
|
||||
using Recyclarr.TestLibrary;
|
||||
using Recyclarr.TrashGuide.MediaNaming;
|
||||
|
||||
namespace Recyclarr.IntegrationTests.TrashGuide;
|
||||
|
||||
[TestFixture]
|
||||
[Parallelizable(ParallelScope.All)]
|
||||
public class MediaNamingGuideServiceTest : IntegrationTestFixture
|
||||
{
|
||||
private void SetupMetadata()
|
||||
{
|
||||
var repo = Resolve<ITrashGuidesRepo>();
|
||||
const string metadataJson =
|
||||
"""
|
||||
{
|
||||
"json_paths": {
|
||||
"radarr": {
|
||||
"naming": ["radarr/naming1", "radarr/naming2"]
|
||||
},
|
||||
"sonarr": {
|
||||
"naming": ["sonarr/naming1", "sonarr/naming2"]
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
Fs.AddFile(repo.Path.File("metadata.json"), new MockFileData(metadataJson));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Radarr_naming()
|
||||
{
|
||||
SetupMetadata();
|
||||
|
||||
var repo = Resolve<ITrashGuidesRepo>();
|
||||
var jsonPath = repo.Path.SubDir("radarr");
|
||||
Fs.AddSameFileFromEmbeddedResource(jsonPath.SubDir("naming1").File("radarr_naming1.json"), GetType());
|
||||
Fs.AddSameFileFromEmbeddedResource(jsonPath.SubDir("naming2").File("radarr_naming2.json"), GetType());
|
||||
|
||||
var sut = Resolve<MediaNamingGuideService>();
|
||||
|
||||
var result = sut.GetRadarrNamingData();
|
||||
result.Should().BeEquivalentTo(new RadarrMediaNamingData
|
||||
{
|
||||
Folder = new Dictionary<string, string>
|
||||
{
|
||||
{"default", "folder_default"},
|
||||
{"plex", "folder_plex"},
|
||||
{"emby", "folder_emby"}
|
||||
},
|
||||
File = new Dictionary<string, string>
|
||||
{
|
||||
{"default", "file_default"},
|
||||
{"emby", "file_emby"},
|
||||
{"jellyfin", "file_jellyfin"}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Sonarr_naming()
|
||||
{
|
||||
SetupMetadata();
|
||||
|
||||
var repo = Resolve<ITrashGuidesRepo>();
|
||||
var jsonPath = repo.Path.SubDir("sonarr");
|
||||
Fs.AddSameFileFromEmbeddedResource(jsonPath.SubDir("naming1").File("sonarr_naming1.json"), GetType());
|
||||
Fs.AddSameFileFromEmbeddedResource(jsonPath.SubDir("naming2").File("sonarr_naming2.json"), GetType());
|
||||
|
||||
var sut = Resolve<MediaNamingGuideService>();
|
||||
|
||||
var result = sut.GetSonarrNamingData();
|
||||
result.Should().BeEquivalentTo(new SonarrMediaNamingData
|
||||
{
|
||||
Season = new Dictionary<string, string>
|
||||
{
|
||||
{"default", "season_default"}
|
||||
},
|
||||
Series = new Dictionary<string, string>
|
||||
{
|
||||
{"default", "series_default"},
|
||||
{"plex", "series_plex"},
|
||||
{"emby", "series_emby"}
|
||||
},
|
||||
Episodes = new SonarrEpisodeNamingData
|
||||
{
|
||||
Standard = new Dictionary<string, string>
|
||||
{
|
||||
{"default:3", "episodes_standard_default_3"},
|
||||
{"default:4", "episodes_standard_default_4"},
|
||||
{"original", "episodes_standard_original"}
|
||||
},
|
||||
Daily = new Dictionary<string, string>
|
||||
{
|
||||
{"default:3", "episodes_daily_default_3"},
|
||||
{"default:4", "episodes_daily_default_4"},
|
||||
{"original", "episodes_daily_original"}
|
||||
},
|
||||
Anime = new Dictionary<string, string>
|
||||
{
|
||||
{"default:3", "episodes_anime_default_3"},
|
||||
{"default:4", "episodes_anime_default_4"}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
using Recyclarr.Config.Parsing;
|
||||
using Recyclarr.Config.Parsing.PostProcessing.ConfigMerging;
|
||||
|
||||
namespace Recyclarr.Tests.Config.Parsing.PostProcessing.ConfigMerging;
|
||||
|
||||
[TestFixture]
|
||||
[Parallelizable(ParallelScope.All)]
|
||||
public class MergeApiKeyTest
|
||||
{
|
||||
[Test]
|
||||
public void Empty_right_to_non_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml
|
||||
{
|
||||
ApiKey = "a"
|
||||
};
|
||||
|
||||
var rightConfig = new SonarrConfigYaml();
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(leftConfig);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Non_empty_right_to_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml();
|
||||
|
||||
// API Key should not be merged!
|
||||
var rightConfig = new SonarrConfigYaml
|
||||
{
|
||||
ApiKey = "b"
|
||||
};
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(leftConfig);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Non_empty_right_to_non_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml
|
||||
{
|
||||
ApiKey = "a"
|
||||
};
|
||||
|
||||
// API Key should not be merged!
|
||||
var rightConfig = new SonarrConfigYaml
|
||||
{
|
||||
ApiKey = "b"
|
||||
};
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(leftConfig);
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
using Recyclarr.Config.Parsing;
|
||||
using Recyclarr.Config.Parsing.PostProcessing.ConfigMerging;
|
||||
|
||||
namespace Recyclarr.Tests.Config.Parsing.PostProcessing.ConfigMerging;
|
||||
|
||||
[TestFixture]
|
||||
[Parallelizable(ParallelScope.All)]
|
||||
public class MergeBaseUrlTest
|
||||
{
|
||||
[Test]
|
||||
public void Empty_right_to_non_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml
|
||||
{
|
||||
BaseUrl = "a"
|
||||
};
|
||||
|
||||
var rightConfig = new SonarrConfigYaml();
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(leftConfig);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Non_empty_right_to_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml();
|
||||
|
||||
// BaseUrl should not be merged!
|
||||
var rightConfig = new SonarrConfigYaml
|
||||
{
|
||||
BaseUrl = "b"
|
||||
};
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(leftConfig);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Non_empty_right_to_non_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml
|
||||
{
|
||||
BaseUrl = "a"
|
||||
};
|
||||
|
||||
// Baseurl should not be merged!
|
||||
var rightConfig = new SonarrConfigYaml
|
||||
{
|
||||
BaseUrl = "b"
|
||||
};
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(leftConfig);
|
||||
}
|
||||
}
|
@ -0,0 +1,190 @@
|
||||
using Recyclarr.Config.Parsing;
|
||||
using Recyclarr.Config.Parsing.PostProcessing.ConfigMerging;
|
||||
|
||||
namespace Recyclarr.Tests.Config.Parsing.PostProcessing.ConfigMerging;
|
||||
|
||||
[TestFixture]
|
||||
[Parallelizable(ParallelScope.All)]
|
||||
public class MergeCustomFormatsTest
|
||||
{
|
||||
[Test]
|
||||
public void Empty_right_to_non_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml
|
||||
{
|
||||
CustomFormats = new[]
|
||||
{
|
||||
new CustomFormatConfigYaml
|
||||
{
|
||||
TrashIds = new[] {"id1", "id2"},
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityScoreConfigYaml {Name = "c", Score = 100}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var rightConfig = new SonarrConfigYaml();
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(leftConfig);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Non_empty_right_to_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml();
|
||||
|
||||
var rightConfig = new SonarrConfigYaml
|
||||
{
|
||||
CustomFormats = new[]
|
||||
{
|
||||
new CustomFormatConfigYaml
|
||||
{
|
||||
TrashIds = new[] {"id1", "id2"},
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityScoreConfigYaml {Name = "c", Score = 100}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(rightConfig);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Non_empty_right_to_non_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml
|
||||
{
|
||||
CustomFormats = new[]
|
||||
{
|
||||
new CustomFormatConfigYaml
|
||||
{
|
||||
TrashIds = new[] {"id1", "id2"},
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityScoreConfigYaml {Name = "c", Score = 100},
|
||||
new QualityScoreConfigYaml {Name = "d", Score = 101},
|
||||
new QualityScoreConfigYaml {Name = "e", Score = 102}
|
||||
}
|
||||
},
|
||||
new CustomFormatConfigYaml
|
||||
{
|
||||
TrashIds = new[] {"id2"},
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityScoreConfigYaml {Name = "f", Score = 100}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var rightConfig = new SonarrConfigYaml
|
||||
{
|
||||
CustomFormats = new[]
|
||||
{
|
||||
new CustomFormatConfigYaml
|
||||
{
|
||||
TrashIds = new[] {"id3", "id4"},
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityScoreConfigYaml {Name = "d", Score = 200}
|
||||
}
|
||||
},
|
||||
new CustomFormatConfigYaml
|
||||
{
|
||||
TrashIds = new[] {"id5", "id6"},
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityScoreConfigYaml {Name = "e", Score = 300}
|
||||
}
|
||||
},
|
||||
new CustomFormatConfigYaml
|
||||
{
|
||||
TrashIds = new[] {"id1"},
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityScoreConfigYaml {Name = "c", Score = 50}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(new SonarrConfigYaml
|
||||
{
|
||||
CustomFormats = new[]
|
||||
{
|
||||
new CustomFormatConfigYaml
|
||||
{
|
||||
TrashIds = new[] {"id2"},
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityScoreConfigYaml {Name = "c", Score = 100}
|
||||
}
|
||||
},
|
||||
new CustomFormatConfigYaml
|
||||
{
|
||||
TrashIds = new[] {"id1", "id2"},
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityScoreConfigYaml {Name = "d", Score = 101}
|
||||
}
|
||||
},
|
||||
new CustomFormatConfigYaml
|
||||
{
|
||||
TrashIds = new[] {"id1", "id2"},
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityScoreConfigYaml {Name = "e", Score = 102}
|
||||
}
|
||||
},
|
||||
new CustomFormatConfigYaml
|
||||
{
|
||||
TrashIds = new[] {"id2"},
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityScoreConfigYaml {Name = "f", Score = 100}
|
||||
}
|
||||
},
|
||||
new CustomFormatConfigYaml
|
||||
{
|
||||
TrashIds = new[] {"id3", "id4"},
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityScoreConfigYaml {Name = "d", Score = 200}
|
||||
}
|
||||
},
|
||||
new CustomFormatConfigYaml
|
||||
{
|
||||
TrashIds = new[] {"id5", "id6"},
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityScoreConfigYaml {Name = "e", Score = 300}
|
||||
}
|
||||
},
|
||||
new CustomFormatConfigYaml
|
||||
{
|
||||
TrashIds = new[] {"id1"},
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityScoreConfigYaml {Name = "c", Score = 50}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Recyclarr.Config.Parsing;
|
||||
using Recyclarr.Config.Parsing.PostProcessing.ConfigMerging;
|
||||
|
||||
namespace Recyclarr.Tests.Config.Parsing.PostProcessing.ConfigMerging;
|
||||
|
||||
[TestFixture]
|
||||
[Parallelizable(ParallelScope.All)]
|
||||
public class MergeMediaNamingRadarrTest
|
||||
{
|
||||
[Test]
|
||||
public void Empty_right_to_non_empty_left()
|
||||
{
|
||||
var leftConfig = new RadarrConfigYaml
|
||||
{
|
||||
MediaNaming = new RadarrMediaNamingConfigYaml
|
||||
{
|
||||
Folder = "folder1",
|
||||
Movie = new RadarrMovieNamingConfigYaml
|
||||
{
|
||||
Rename = false,
|
||||
Format = "format1"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var rightConfig = new RadarrConfigYaml();
|
||||
|
||||
var sut = new RadarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(leftConfig);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Non_empty_right_to_empty_left()
|
||||
{
|
||||
var leftConfig = new RadarrConfigYaml();
|
||||
|
||||
var rightConfig = new RadarrConfigYaml
|
||||
{
|
||||
MediaNaming = new RadarrMediaNamingConfigYaml
|
||||
{
|
||||
Folder = "folder1",
|
||||
Movie = new RadarrMovieNamingConfigYaml
|
||||
{
|
||||
Rename = false,
|
||||
Format = "format1"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var sut = new RadarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(rightConfig);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")]
|
||||
public void Non_empty_right_to_non_empty_left()
|
||||
{
|
||||
var leftConfig = new RadarrConfigYaml
|
||||
{
|
||||
MediaNaming = new RadarrMediaNamingConfigYaml
|
||||
{
|
||||
Folder = "folder1",
|
||||
Movie = new RadarrMovieNamingConfigYaml
|
||||
{
|
||||
Rename = false,
|
||||
Format = "format1"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var rightConfig = new RadarrConfigYaml
|
||||
{
|
||||
MediaNaming = new RadarrMediaNamingConfigYaml
|
||||
{
|
||||
Folder = "folder2",
|
||||
Movie = new RadarrMovieNamingConfigYaml
|
||||
{
|
||||
Rename = false,
|
||||
Format = "format2"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var sut = new RadarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(rightConfig);
|
||||
}
|
||||
}
|
@ -0,0 +1,109 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Recyclarr.Config.Parsing;
|
||||
using Recyclarr.Config.Parsing.PostProcessing.ConfigMerging;
|
||||
|
||||
namespace Recyclarr.Tests.Config.Parsing.PostProcessing.ConfigMerging;
|
||||
|
||||
[TestFixture]
|
||||
[Parallelizable(ParallelScope.All)]
|
||||
public class MergeMediaNamingSonarrTest
|
||||
{
|
||||
[Test]
|
||||
public void Empty_right_to_non_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml
|
||||
{
|
||||
MediaNaming = new SonarrMediaNamingConfigYaml
|
||||
{
|
||||
Series = "series1",
|
||||
Season = "season1",
|
||||
Episodes = new SonarrEpisodeNamingConfigYaml
|
||||
{
|
||||
Rename = false,
|
||||
Standard = "standard1",
|
||||
Daily = "daily1",
|
||||
Anime = "anime1"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var rightConfig = new SonarrConfigYaml();
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(leftConfig);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Non_empty_right_to_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml();
|
||||
|
||||
var rightConfig = new SonarrConfigYaml
|
||||
{
|
||||
MediaNaming = new SonarrMediaNamingConfigYaml
|
||||
{
|
||||
Series = "series1",
|
||||
Season = "season1",
|
||||
Episodes = new SonarrEpisodeNamingConfigYaml
|
||||
{
|
||||
Rename = false,
|
||||
Standard = "standard1",
|
||||
Daily = "daily1",
|
||||
Anime = "anime1"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(rightConfig);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")]
|
||||
public void Non_empty_right_to_non_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml
|
||||
{
|
||||
MediaNaming = new SonarrMediaNamingConfigYaml
|
||||
{
|
||||
Series = "series1",
|
||||
Season = "season1",
|
||||
Episodes = new SonarrEpisodeNamingConfigYaml
|
||||
{
|
||||
Rename = false,
|
||||
Standard = "standard1",
|
||||
Daily = "daily1",
|
||||
Anime = "anime1"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var rightConfig = new SonarrConfigYaml
|
||||
{
|
||||
MediaNaming = new SonarrMediaNamingConfigYaml
|
||||
{
|
||||
Series = "series2",
|
||||
Season = "season2",
|
||||
Episodes = new SonarrEpisodeNamingConfigYaml
|
||||
{
|
||||
Rename = false,
|
||||
Standard = "standard2",
|
||||
Daily = "daily2",
|
||||
Anime = "anime2"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(rightConfig);
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
using Recyclarr.Config.Parsing;
|
||||
using Recyclarr.Config.Parsing.PostProcessing.ConfigMerging;
|
||||
|
||||
namespace Recyclarr.Tests.Config.Parsing.PostProcessing.ConfigMerging;
|
||||
|
||||
[TestFixture]
|
||||
[Parallelizable(ParallelScope.All)]
|
||||
public class MergeQualityDefinitionTest
|
||||
{
|
||||
[Test]
|
||||
public void Empty_right_to_non_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml
|
||||
{
|
||||
QualityDefinition = new QualitySizeConfigYaml
|
||||
{
|
||||
Type = "type1",
|
||||
PreferredRatio = 0.5m
|
||||
}
|
||||
};
|
||||
|
||||
var rightConfig = new SonarrConfigYaml();
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(leftConfig);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Non_empty_right_to_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml();
|
||||
|
||||
var rightConfig = new SonarrConfigYaml
|
||||
{
|
||||
QualityDefinition = new QualitySizeConfigYaml
|
||||
{
|
||||
Type = "type1",
|
||||
PreferredRatio = 0.5m
|
||||
}
|
||||
};
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(rightConfig);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Non_empty_right_to_non_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml
|
||||
{
|
||||
QualityDefinition = new QualitySizeConfigYaml
|
||||
{
|
||||
Type = "type1",
|
||||
PreferredRatio = 0.5m
|
||||
}
|
||||
};
|
||||
|
||||
var rightConfig = new SonarrConfigYaml
|
||||
{
|
||||
QualityDefinition = new QualitySizeConfigYaml
|
||||
{
|
||||
Type = "type2",
|
||||
PreferredRatio = 1.0m
|
||||
}
|
||||
};
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(rightConfig);
|
||||
}
|
||||
}
|
@ -0,0 +1,221 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Recyclarr.Config.Models;
|
||||
using Recyclarr.Config.Parsing;
|
||||
using Recyclarr.Config.Parsing.PostProcessing.ConfigMerging;
|
||||
|
||||
namespace Recyclarr.Tests.Config.Parsing.PostProcessing.ConfigMerging;
|
||||
|
||||
[TestFixture]
|
||||
[Parallelizable(ParallelScope.All)]
|
||||
public class MergeQualityProfilesTest
|
||||
{
|
||||
[Test]
|
||||
public void Empty_right_to_non_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml
|
||||
{
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityProfileConfigYaml
|
||||
{
|
||||
Name = "e",
|
||||
QualitySort = QualitySortAlgorithm.Top,
|
||||
MinFormatScore = 100,
|
||||
ScoreSet = "set1",
|
||||
ResetUnmatchedScores = new ResetUnmatchedScoresConfigYaml
|
||||
{
|
||||
Enabled = true,
|
||||
Except = new[] {"except1"}
|
||||
},
|
||||
Upgrade = new QualityProfileFormatUpgradeYaml
|
||||
{
|
||||
Allowed = true,
|
||||
UntilQuality = "quality1",
|
||||
UntilScore = 200
|
||||
},
|
||||
Qualities = new[]
|
||||
{
|
||||
new QualityProfileQualityConfigYaml
|
||||
{
|
||||
Enabled = true,
|
||||
Name = "quality1",
|
||||
Qualities = new[] {"quality"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var rightConfig = new SonarrConfigYaml();
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(leftConfig);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Non_empty_right_to_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml();
|
||||
|
||||
var rightConfig = new SonarrConfigYaml
|
||||
{
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityProfileConfigYaml
|
||||
{
|
||||
Name = "e",
|
||||
QualitySort = QualitySortAlgorithm.Top,
|
||||
MinFormatScore = 100,
|
||||
ScoreSet = "set1",
|
||||
ResetUnmatchedScores = new ResetUnmatchedScoresConfigYaml
|
||||
{
|
||||
Enabled = true,
|
||||
Except = new[] {"except1"}
|
||||
},
|
||||
Upgrade = new QualityProfileFormatUpgradeYaml
|
||||
{
|
||||
Allowed = true,
|
||||
UntilQuality = "quality1",
|
||||
UntilScore = 200
|
||||
},
|
||||
Qualities = new[]
|
||||
{
|
||||
new QualityProfileQualityConfigYaml
|
||||
{
|
||||
Enabled = true,
|
||||
Name = "quality1",
|
||||
Qualities = new[] {"quality"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(rightConfig);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")]
|
||||
public void Non_empty_right_to_non_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml
|
||||
{
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityProfileConfigYaml
|
||||
{
|
||||
Name = "e",
|
||||
QualitySort = QualitySortAlgorithm.Top,
|
||||
MinFormatScore = 100,
|
||||
ScoreSet = "set1",
|
||||
ResetUnmatchedScores = new ResetUnmatchedScoresConfigYaml
|
||||
{
|
||||
Enabled = true,
|
||||
Except = new[] {"except1"}
|
||||
},
|
||||
Upgrade = new QualityProfileFormatUpgradeYaml
|
||||
{
|
||||
Allowed = true,
|
||||
UntilQuality = "quality1",
|
||||
UntilScore = 200
|
||||
},
|
||||
Qualities = new[]
|
||||
{
|
||||
new QualityProfileQualityConfigYaml
|
||||
{
|
||||
Enabled = true,
|
||||
Name = "quality1",
|
||||
Qualities = new[] {"quality"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var rightConfig = new SonarrConfigYaml
|
||||
{
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityProfileConfigYaml
|
||||
{
|
||||
Name = "e",
|
||||
ScoreSet = "set2",
|
||||
ResetUnmatchedScores = new ResetUnmatchedScoresConfigYaml
|
||||
{
|
||||
Except = new[] {"except2", "except3"}
|
||||
},
|
||||
Upgrade = new QualityProfileFormatUpgradeYaml
|
||||
{
|
||||
UntilQuality = "quality2"
|
||||
},
|
||||
Qualities = new[]
|
||||
{
|
||||
new QualityProfileQualityConfigYaml
|
||||
{
|
||||
Enabled = false,
|
||||
Name = "quality2",
|
||||
Qualities = new[] {"quality3"}
|
||||
},
|
||||
new QualityProfileQualityConfigYaml
|
||||
{
|
||||
Enabled = true,
|
||||
Name = "quality4",
|
||||
Qualities = new[] {"quality5", "quality6"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(new SonarrConfigYaml
|
||||
{
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityProfileConfigYaml
|
||||
{
|
||||
Name = "e",
|
||||
QualitySort = QualitySortAlgorithm.Top,
|
||||
MinFormatScore = 100,
|
||||
ScoreSet = "set2",
|
||||
ResetUnmatchedScores = new ResetUnmatchedScoresConfigYaml
|
||||
{
|
||||
Enabled = true,
|
||||
Except = new[] {"except1", "except2", "except3"}
|
||||
},
|
||||
Upgrade = new QualityProfileFormatUpgradeYaml
|
||||
{
|
||||
Allowed = true,
|
||||
UntilQuality = "quality2",
|
||||
UntilScore = 200
|
||||
},
|
||||
Qualities = new[]
|
||||
{
|
||||
new QualityProfileQualityConfigYaml
|
||||
{
|
||||
Enabled = false,
|
||||
Name = "quality2",
|
||||
Qualities = new[] {"quality3"}
|
||||
},
|
||||
new QualityProfileQualityConfigYaml
|
||||
{
|
||||
Enabled = true,
|
||||
Name = "quality4",
|
||||
Qualities = new[] {"quality5", "quality6"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -1,595 +0,0 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using FluentAssertions.Execution;
|
||||
using Recyclarr.Config.Models;
|
||||
using Recyclarr.Config.Parsing;
|
||||
using Recyclarr.Config.Parsing.PostProcessing.ConfigMerging;
|
||||
|
||||
namespace Recyclarr.Tests.Config.Parsing.PostProcessing.ConfigMerging;
|
||||
|
||||
[TestFixture]
|
||||
[Parallelizable(ParallelScope.All)]
|
||||
public class ServiceConfigMergerTest
|
||||
{
|
||||
[Test]
|
||||
public void Merge_api_key_from_empty_right_to_non_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml
|
||||
{
|
||||
ApiKey = "a"
|
||||
};
|
||||
|
||||
var rightConfig = new SonarrConfigYaml();
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(leftConfig);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Merge_api_key_from_non_empty_right_to_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml();
|
||||
|
||||
// API Key should not be merged!
|
||||
var rightConfig = new SonarrConfigYaml
|
||||
{
|
||||
ApiKey = "b"
|
||||
};
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(leftConfig);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Merge_api_key_from_non_empty_right_to_non_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml
|
||||
{
|
||||
ApiKey = "a"
|
||||
};
|
||||
|
||||
// API Key should not be merged!
|
||||
var rightConfig = new SonarrConfigYaml
|
||||
{
|
||||
ApiKey = "b"
|
||||
};
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(leftConfig);
|
||||
}
|
||||
|
||||
//------------------------------------------------------
|
||||
|
||||
[Test]
|
||||
public void Merge_base_url_from_empty_right_to_non_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml
|
||||
{
|
||||
BaseUrl = "a"
|
||||
};
|
||||
|
||||
var rightConfig = new SonarrConfigYaml();
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(leftConfig);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Merge_base_url_from_non_empty_right_to_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml();
|
||||
|
||||
// BaseUrl should not be merged!
|
||||
var rightConfig = new SonarrConfigYaml
|
||||
{
|
||||
BaseUrl = "b"
|
||||
};
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(leftConfig);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Merge_base_url_from_non_empty_right_to_non_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml
|
||||
{
|
||||
BaseUrl = "a"
|
||||
};
|
||||
|
||||
// Baseurl should not be merged!
|
||||
var rightConfig = new SonarrConfigYaml
|
||||
{
|
||||
BaseUrl = "b"
|
||||
};
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(leftConfig);
|
||||
}
|
||||
|
||||
//------------------------------------------------------
|
||||
|
||||
[Test]
|
||||
public void Merge_quality_definition_from_empty_right_to_non_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml
|
||||
{
|
||||
QualityDefinition = new QualitySizeConfigYaml
|
||||
{
|
||||
Type = "type1",
|
||||
PreferredRatio = 0.5m
|
||||
}
|
||||
};
|
||||
|
||||
var rightConfig = new SonarrConfigYaml();
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(leftConfig);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Merge_quality_definition_from_non_empty_right_to_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml();
|
||||
|
||||
var rightConfig = new SonarrConfigYaml
|
||||
{
|
||||
QualityDefinition = new QualitySizeConfigYaml
|
||||
{
|
||||
Type = "type1",
|
||||
PreferredRatio = 0.5m
|
||||
}
|
||||
};
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(rightConfig);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Merge_quality_definition_from_non_empty_right_to_non_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml
|
||||
{
|
||||
QualityDefinition = new QualitySizeConfigYaml
|
||||
{
|
||||
Type = "type1",
|
||||
PreferredRatio = 0.5m
|
||||
}
|
||||
};
|
||||
|
||||
var rightConfig = new SonarrConfigYaml
|
||||
{
|
||||
QualityDefinition = new QualitySizeConfigYaml
|
||||
{
|
||||
Type = "type2",
|
||||
PreferredRatio = 1.0m
|
||||
}
|
||||
};
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(rightConfig);
|
||||
}
|
||||
|
||||
//------------------------------------------------------
|
||||
|
||||
[Test]
|
||||
public void Merge_custom_formats_from_empty_right_to_non_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml
|
||||
{
|
||||
CustomFormats = new[]
|
||||
{
|
||||
new CustomFormatConfigYaml
|
||||
{
|
||||
TrashIds = new[] {"id1", "id2"},
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityScoreConfigYaml {Name = "c", Score = 100}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var rightConfig = new SonarrConfigYaml();
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(leftConfig);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Merge_custom_formats_from_non_empty_right_to_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml();
|
||||
|
||||
var rightConfig = new SonarrConfigYaml
|
||||
{
|
||||
CustomFormats = new[]
|
||||
{
|
||||
new CustomFormatConfigYaml
|
||||
{
|
||||
TrashIds = new[] {"id1", "id2"},
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityScoreConfigYaml {Name = "c", Score = 100}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(rightConfig);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Merge_custom_formats_from_non_empty_right_to_non_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml
|
||||
{
|
||||
CustomFormats = new[]
|
||||
{
|
||||
new CustomFormatConfigYaml
|
||||
{
|
||||
TrashIds = new[] {"id1", "id2"},
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityScoreConfigYaml {Name = "c", Score = 100},
|
||||
new QualityScoreConfigYaml {Name = "d", Score = 101},
|
||||
new QualityScoreConfigYaml {Name = "e", Score = 102}
|
||||
}
|
||||
},
|
||||
new CustomFormatConfigYaml
|
||||
{
|
||||
TrashIds = new[] {"id2"},
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityScoreConfigYaml {Name = "f", Score = 100}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var rightConfig = new SonarrConfigYaml
|
||||
{
|
||||
CustomFormats = new[]
|
||||
{
|
||||
new CustomFormatConfigYaml
|
||||
{
|
||||
TrashIds = new[] {"id3", "id4"},
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityScoreConfigYaml {Name = "d", Score = 200}
|
||||
}
|
||||
},
|
||||
new CustomFormatConfigYaml
|
||||
{
|
||||
TrashIds = new[] {"id5", "id6"},
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityScoreConfigYaml {Name = "e", Score = 300}
|
||||
}
|
||||
},
|
||||
new CustomFormatConfigYaml
|
||||
{
|
||||
TrashIds = new[] {"id1"},
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityScoreConfigYaml {Name = "c", Score = 50}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(new SonarrConfigYaml
|
||||
{
|
||||
CustomFormats = new[]
|
||||
{
|
||||
new CustomFormatConfigYaml
|
||||
{
|
||||
TrashIds = new[] {"id2"},
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityScoreConfigYaml {Name = "c", Score = 100}
|
||||
}
|
||||
},
|
||||
new CustomFormatConfigYaml
|
||||
{
|
||||
TrashIds = new[] {"id1", "id2"},
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityScoreConfigYaml {Name = "d", Score = 101}
|
||||
}
|
||||
},
|
||||
new CustomFormatConfigYaml
|
||||
{
|
||||
TrashIds = new[] {"id1", "id2"},
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityScoreConfigYaml {Name = "e", Score = 102}
|
||||
}
|
||||
},
|
||||
new CustomFormatConfigYaml
|
||||
{
|
||||
TrashIds = new[] {"id2"},
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityScoreConfigYaml {Name = "f", Score = 100}
|
||||
}
|
||||
},
|
||||
new CustomFormatConfigYaml
|
||||
{
|
||||
TrashIds = new[] {"id3", "id4"},
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityScoreConfigYaml {Name = "d", Score = 200}
|
||||
}
|
||||
},
|
||||
new CustomFormatConfigYaml
|
||||
{
|
||||
TrashIds = new[] {"id5", "id6"},
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityScoreConfigYaml {Name = "e", Score = 300}
|
||||
}
|
||||
},
|
||||
new CustomFormatConfigYaml
|
||||
{
|
||||
TrashIds = new[] {"id1"},
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityScoreConfigYaml {Name = "c", Score = 50}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//------------------------------------------------------
|
||||
|
||||
[Test]
|
||||
public void Merge_quality_profiles_from_empty_right_to_non_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml
|
||||
{
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityProfileConfigYaml
|
||||
{
|
||||
Name = "e",
|
||||
QualitySort = QualitySortAlgorithm.Top,
|
||||
MinFormatScore = 100,
|
||||
ScoreSet = "set1",
|
||||
ResetUnmatchedScores = new ResetUnmatchedScoresConfigYaml
|
||||
{
|
||||
Enabled = true,
|
||||
Except = new[] {"except1"}
|
||||
},
|
||||
Upgrade = new QualityProfileFormatUpgradeYaml
|
||||
{
|
||||
Allowed = true,
|
||||
UntilQuality = "quality1",
|
||||
UntilScore = 200
|
||||
},
|
||||
Qualities = new[]
|
||||
{
|
||||
new QualityProfileQualityConfigYaml
|
||||
{
|
||||
Enabled = true,
|
||||
Name = "quality1",
|
||||
Qualities = new[] {"quality"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var rightConfig = new SonarrConfigYaml();
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(leftConfig);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Merge_quality_profiles_from_non_empty_right_to_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml();
|
||||
|
||||
var rightConfig = new SonarrConfigYaml
|
||||
{
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityProfileConfigYaml
|
||||
{
|
||||
Name = "e",
|
||||
QualitySort = QualitySortAlgorithm.Top,
|
||||
MinFormatScore = 100,
|
||||
ScoreSet = "set1",
|
||||
ResetUnmatchedScores = new ResetUnmatchedScoresConfigYaml
|
||||
{
|
||||
Enabled = true,
|
||||
Except = new[] {"except1"}
|
||||
},
|
||||
Upgrade = new QualityProfileFormatUpgradeYaml
|
||||
{
|
||||
Allowed = true,
|
||||
UntilQuality = "quality1",
|
||||
UntilScore = 200
|
||||
},
|
||||
Qualities = new[]
|
||||
{
|
||||
new QualityProfileQualityConfigYaml
|
||||
{
|
||||
Enabled = true,
|
||||
Name = "quality1",
|
||||
Qualities = new[] {"quality"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(rightConfig);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")]
|
||||
public void Merge_quality_profiles_from_non_empty_right_to_non_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml
|
||||
{
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityProfileConfigYaml
|
||||
{
|
||||
Name = "e",
|
||||
QualitySort = QualitySortAlgorithm.Top,
|
||||
MinFormatScore = 100,
|
||||
ScoreSet = "set1",
|
||||
ResetUnmatchedScores = new ResetUnmatchedScoresConfigYaml
|
||||
{
|
||||
Enabled = true,
|
||||
Except = new[] {"except1"}
|
||||
},
|
||||
Upgrade = new QualityProfileFormatUpgradeYaml
|
||||
{
|
||||
Allowed = true,
|
||||
UntilQuality = "quality1",
|
||||
UntilScore = 200
|
||||
},
|
||||
Qualities = new[]
|
||||
{
|
||||
new QualityProfileQualityConfigYaml
|
||||
{
|
||||
Enabled = true,
|
||||
Name = "quality1",
|
||||
Qualities = new[] {"quality"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var rightConfig = new SonarrConfigYaml
|
||||
{
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityProfileConfigYaml
|
||||
{
|
||||
Name = "e",
|
||||
ScoreSet = "set2",
|
||||
ResetUnmatchedScores = new ResetUnmatchedScoresConfigYaml
|
||||
{
|
||||
Except = new[] {"except2", "except3"}
|
||||
},
|
||||
Upgrade = new QualityProfileFormatUpgradeYaml
|
||||
{
|
||||
UntilQuality = "quality2"
|
||||
},
|
||||
Qualities = new[]
|
||||
{
|
||||
new QualityProfileQualityConfigYaml
|
||||
{
|
||||
Enabled = false,
|
||||
Name = "quality2",
|
||||
Qualities = new[] {"quality3"}
|
||||
},
|
||||
new QualityProfileQualityConfigYaml
|
||||
{
|
||||
Enabled = true,
|
||||
Name = "quality4",
|
||||
Qualities = new[] {"quality5", "quality6"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
using var scope = new AssertionScope().UsingLineBreaks;
|
||||
|
||||
result.Should().BeEquivalentTo(new SonarrConfigYaml
|
||||
{
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityProfileConfigYaml
|
||||
{
|
||||
Name = "e",
|
||||
QualitySort = QualitySortAlgorithm.Top,
|
||||
MinFormatScore = 100,
|
||||
ScoreSet = "set2",
|
||||
ResetUnmatchedScores = new ResetUnmatchedScoresConfigYaml
|
||||
{
|
||||
Enabled = true,
|
||||
Except = new[] {"except1", "except2", "except3"}
|
||||
},
|
||||
Upgrade = new QualityProfileFormatUpgradeYaml
|
||||
{
|
||||
Allowed = true,
|
||||
UntilQuality = "quality2",
|
||||
UntilScore = 200
|
||||
},
|
||||
Qualities = new[]
|
||||
{
|
||||
new QualityProfileQualityConfigYaml
|
||||
{
|
||||
Enabled = false,
|
||||
Name = "quality2",
|
||||
Qualities = new[] {"quality3"}
|
||||
},
|
||||
new QualityProfileQualityConfigYaml
|
||||
{
|
||||
Enabled = true,
|
||||
Name = "quality4",
|
||||
Qualities = new[] {"quality5", "quality6"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
using System.IO.Abstractions;
|
||||
using Recyclarr.Repo;
|
||||
|
||||
namespace Recyclarr.Tests.Repo;
|
||||
|
||||
[TestFixture]
|
||||
[Parallelizable(ParallelScope.All)]
|
||||
public class TrashRepoMetadataBuilderTest
|
||||
{
|
||||
private const string MetadataJson =
|
||||
"""
|
||||
{
|
||||
"$schema": "metadata.schema.json",
|
||||
"json_paths": {
|
||||
"radarr": {
|
||||
"custom_formats": ["docs/json/radarr/cf"],
|
||||
"qualities": ["docs/json/radarr/quality-size"],
|
||||
"naming": ["docs/json/radarr/naming"]
|
||||
},
|
||||
"sonarr": {
|
||||
"release_profiles": ["docs/json/sonarr/rp"],
|
||||
"custom_formats": ["docs/json/sonarr/cf"],
|
||||
"qualities": ["docs/json/sonarr/quality-size"],
|
||||
"naming": ["docs/json/sonarr/naming"]
|
||||
}
|
||||
},
|
||||
"recyclarr": {
|
||||
"templates": "docs/recyclarr-configs"
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
[Test, AutoMockData]
|
||||
public void Naming_is_parsed(
|
||||
[Frozen] ITrashGuidesRepo repo,
|
||||
MockFileSystem fs,
|
||||
TrashRepoMetadataBuilder sut)
|
||||
{
|
||||
fs.AddFile(repo.Path.File("metadata.json"), new MockFileData(MetadataJson));
|
||||
|
||||
var result = sut.GetMetadata();
|
||||
|
||||
result.JsonPaths.Radarr.Naming.Should().BeEquivalentTo("docs/json/radarr/naming");
|
||||
result.JsonPaths.Sonarr.Naming.Should().BeEquivalentTo("docs/json/sonarr/naming");
|
||||
}
|
||||
}
|
Loading…
Reference in new issue