parent
147b3fc859
commit
d45563cf1c
@ -1,50 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$id": "https://raw.githubusercontent.com/recyclarr/recyclarr/master/schemas/config/release-profiles.json",
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"additionalProperties": false,
|
||||
"required": ["trash_ids"],
|
||||
"properties": {
|
||||
"trash_ids": {
|
||||
"$ref": "trash-ids.json"
|
||||
},
|
||||
"strict_negative_scores": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Enables preferred term scores less than 0 to be instead treated as \"Must Not Contain\" (ignored) terms."
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"description": "A list of one or more strings representing tags that will be applied to this release profile.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"filter": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Defines various ways that release profile terms from the guide are synchronized with Sonarr.",
|
||||
"oneOf": [
|
||||
{
|
||||
"required": ["include"]
|
||||
},
|
||||
{
|
||||
"required": ["exclude"]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"include": {
|
||||
"$ref": "trash-ids.json",
|
||||
"description": "A list of trash_id values representing terms (Required, Ignored, or Preferred) that should be included in the created Release Profile in Sonarr."
|
||||
},
|
||||
"exclude": {
|
||||
"$ref": "trash-ids.json",
|
||||
"description": "A list of trash_id values representing terms (Required, Ignored, or Preferred) that should be excluded from the created Release Profile in Sonarr."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using JetBrains.Annotations;
|
||||
using Recyclarr.Cli.Pipelines.ReleaseProfile;
|
||||
using Recyclarr.Repo;
|
||||
using Spectre.Console.Cli;
|
||||
|
||||
#pragma warning disable CS8765
|
||||
|
||||
namespace Recyclarr.Cli.Console.Commands;
|
||||
|
||||
[UsedImplicitly]
|
||||
[Description("List Sonarr release profiles in the guide for a particular service.")]
|
||||
public class ListReleaseProfilesCommand(ILogger log, ReleaseProfileDataLister lister, IMultiRepoUpdater repoUpdater)
|
||||
: AsyncCommand<ListReleaseProfilesCommand.CliSettings>
|
||||
{
|
||||
[UsedImplicitly]
|
||||
[SuppressMessage("Design", "CA1034:Nested types should not be visible")]
|
||||
public class CliSettings : BaseCommandSettings
|
||||
{
|
||||
[CommandOption("--terms")]
|
||||
[Description(
|
||||
"For the given Release Profile Trash ID, list terms in it that can be filtered in YAML format. " +
|
||||
"Note that not every release profile has terms that may be filtered.")]
|
||||
[UsedImplicitly(ImplicitUseKindFlags.Assign)]
|
||||
public string? ListTerms { get; init; }
|
||||
}
|
||||
|
||||
public override async Task<int> ExecuteAsync(CommandContext context, CliSettings settings)
|
||||
{
|
||||
await repoUpdater.UpdateAllRepositories(settings.CancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
if (settings.ListTerms is not null)
|
||||
{
|
||||
// Ignore nullability of ListTerms since the Settings.Validate() method will check for null/empty.
|
||||
lister.ListTerms(settings.ListTerms!);
|
||||
}
|
||||
else
|
||||
{
|
||||
lister.ListReleaseProfiles();
|
||||
}
|
||||
}
|
||||
catch (ArgumentException e)
|
||||
{
|
||||
log.Error(e, "Error");
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
@ -1,43 +1,36 @@
|
||||
using Recyclarr.Compatibility.Sonarr;
|
||||
using Recyclarr.Config.Models;
|
||||
using Recyclarr.ServarrApi.MediaNaming;
|
||||
using Recyclarr.TrashGuide.MediaNaming;
|
||||
|
||||
namespace Recyclarr.Cli.Pipelines.MediaNaming.PipelinePhases.Config;
|
||||
|
||||
public class SonarrMediaNamingConfigPhase(ISonarrCapabilityFetcher sonarrCapabilities)
|
||||
: ServiceBasedMediaNamingConfigPhase<SonarrConfiguration>
|
||||
public class SonarrMediaNamingConfigPhase : ServiceBasedMediaNamingConfigPhase<SonarrConfiguration>
|
||||
{
|
||||
protected override async Task<MediaNamingDto> ProcessNaming(
|
||||
protected override Task<MediaNamingDto> ProcessNaming(
|
||||
SonarrConfiguration config,
|
||||
IMediaNamingGuideService guide,
|
||||
NamingFormatLookup lookup)
|
||||
{
|
||||
var guideData = guide.GetSonarrNamingData();
|
||||
var configData = config.MediaNaming;
|
||||
var capabilities = await sonarrCapabilities.GetCapabilities(config);
|
||||
var keySuffix = capabilities.SupportsCustomFormats ? ":4" : ":3";
|
||||
|
||||
return new SonarrMediaNamingDto
|
||||
return Task.FromResult<MediaNamingDto>(new SonarrMediaNamingDto
|
||||
{
|
||||
SeasonFolderFormat = lookup.ObtainFormat(guideData.Season, configData.Season, "Season Folder Format"),
|
||||
SeriesFolderFormat = lookup.ObtainFormat(guideData.Series, configData.Series, "Series Folder Format"),
|
||||
StandardEpisodeFormat = lookup.ObtainFormat(
|
||||
guideData.Episodes.Standard,
|
||||
configData.Episodes?.Standard,
|
||||
keySuffix,
|
||||
"Standard Episode Format"),
|
||||
DailyEpisodeFormat = lookup.ObtainFormat(
|
||||
guideData.Episodes.Daily,
|
||||
configData.Episodes?.Daily,
|
||||
keySuffix,
|
||||
"Daily Episode Format"),
|
||||
AnimeEpisodeFormat = lookup.ObtainFormat(
|
||||
guideData.Episodes.Anime,
|
||||
configData.Episodes?.Anime,
|
||||
keySuffix,
|
||||
"Anime Episode Format"),
|
||||
RenameEpisodes = configData.Episodes?.Rename
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +0,0 @@
|
||||
using Recyclarr.Config.Models;
|
||||
using Recyclarr.TrashGuide.ReleaseProfile;
|
||||
|
||||
namespace Recyclarr.Cli.Pipelines.ReleaseProfile.Filters;
|
||||
|
||||
public interface IReleaseProfileFilter
|
||||
{
|
||||
ReleaseProfileData Transform(ReleaseProfileData profile, ReleaseProfileConfig config);
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
using Recyclarr.Config.Models;
|
||||
using Recyclarr.TrashGuide.ReleaseProfile;
|
||||
|
||||
namespace Recyclarr.Cli.Pipelines.ReleaseProfile.Filters;
|
||||
|
||||
public interface IReleaseProfileFilterPipeline
|
||||
{
|
||||
ReleaseProfileData Process(ReleaseProfileData profile, ReleaseProfileConfig config);
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
using Recyclarr.Config.Models;
|
||||
using Recyclarr.TrashGuide.ReleaseProfile;
|
||||
|
||||
namespace Recyclarr.Cli.Pipelines.ReleaseProfile.Filters;
|
||||
|
||||
public class IncludeExcludeFilter(ILogger log) : IReleaseProfileFilter
|
||||
{
|
||||
private readonly ReleaseProfileDataFilterer _filterer = new(log);
|
||||
|
||||
public ReleaseProfileData Transform(ReleaseProfileData profile, ReleaseProfileConfig config)
|
||||
{
|
||||
if (config.Filter == null)
|
||||
{
|
||||
return profile;
|
||||
}
|
||||
|
||||
log.Debug("This profile will be filtered");
|
||||
var newProfile = _filterer.FilterProfile(profile, config.Filter);
|
||||
return newProfile ?? profile;
|
||||
}
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using Recyclarr.Config.Models;
|
||||
using Recyclarr.TrashGuide.ReleaseProfile;
|
||||
|
||||
namespace Recyclarr.Cli.Pipelines.ReleaseProfile.Filters;
|
||||
|
||||
public class ReleaseProfileDataFilterer(ILogger log)
|
||||
{
|
||||
private readonly ReleaseProfileDataValidationFilterer _validator = new(log);
|
||||
|
||||
public ReadOnlyCollection<TermData> ExcludeTerms(
|
||||
IEnumerable<TermData> terms,
|
||||
IEnumerable<string> excludeFilter)
|
||||
{
|
||||
var result = terms.Where(x => !excludeFilter.Contains(x.TrashId, StringComparer.InvariantCultureIgnoreCase));
|
||||
return _validator.FilterTerms(result).ToList().AsReadOnly();
|
||||
}
|
||||
|
||||
public ReadOnlyCollection<PreferredTermData> ExcludeTerms(
|
||||
IEnumerable<PreferredTermData> terms,
|
||||
IReadOnlyCollection<string> excludeFilter)
|
||||
{
|
||||
var result = terms
|
||||
.Select(x => new PreferredTermData
|
||||
{
|
||||
Score = x.Score,
|
||||
Terms = ExcludeTerms(x.Terms, excludeFilter)
|
||||
});
|
||||
|
||||
return _validator.FilterTerms(result).ToList().AsReadOnly();
|
||||
}
|
||||
|
||||
public ReadOnlyCollection<TermData> IncludeTerms(
|
||||
IEnumerable<TermData> terms,
|
||||
IEnumerable<string> includeFilter)
|
||||
{
|
||||
var result = terms.Where(x => includeFilter.Contains(x.TrashId, StringComparer.InvariantCultureIgnoreCase));
|
||||
return _validator.FilterTerms(result).ToList().AsReadOnly();
|
||||
}
|
||||
|
||||
public ReadOnlyCollection<PreferredTermData> IncludeTerms(
|
||||
IEnumerable<PreferredTermData> terms,
|
||||
IReadOnlyCollection<string> includeFilter)
|
||||
{
|
||||
var result = terms
|
||||
.Select(x => new PreferredTermData
|
||||
{
|
||||
Score = x.Score,
|
||||
Terms = IncludeTerms(x.Terms, includeFilter)
|
||||
});
|
||||
|
||||
return _validator.FilterTerms(result).ToList().AsReadOnly();
|
||||
}
|
||||
|
||||
public ReleaseProfileData? FilterProfile(
|
||||
ReleaseProfileData selectedProfile,
|
||||
SonarrProfileFilterConfig profileFilter)
|
||||
{
|
||||
if (profileFilter.Include.Count != 0)
|
||||
{
|
||||
log.Debug("Using inclusion filter");
|
||||
return selectedProfile with
|
||||
{
|
||||
Required = IncludeTerms(selectedProfile.Required, profileFilter.Include),
|
||||
Ignored = IncludeTerms(selectedProfile.Ignored, profileFilter.Include),
|
||||
Preferred = IncludeTerms(selectedProfile.Preferred, profileFilter.Include)
|
||||
};
|
||||
}
|
||||
|
||||
if (profileFilter.Exclude.Count != 0)
|
||||
{
|
||||
log.Debug("Using exclusion filter");
|
||||
return selectedProfile with
|
||||
{
|
||||
Required = ExcludeTerms(selectedProfile.Required, profileFilter.Exclude),
|
||||
Ignored = ExcludeTerms(selectedProfile.Ignored, profileFilter.Exclude),
|
||||
Preferred = ExcludeTerms(selectedProfile.Preferred, profileFilter.Exclude)
|
||||
};
|
||||
}
|
||||
|
||||
log.Debug("Filter property present but is empty");
|
||||
return null;
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
using Recyclarr.Config.Models;
|
||||
using Recyclarr.TrashGuide.ReleaseProfile;
|
||||
|
||||
namespace Recyclarr.Cli.Pipelines.ReleaseProfile.Filters;
|
||||
|
||||
public class ReleaseProfileFilterPipeline(IOrderedEnumerable<IReleaseProfileFilter> filters)
|
||||
: IReleaseProfileFilterPipeline
|
||||
{
|
||||
public ReleaseProfileData Process(ReleaseProfileData profile, ReleaseProfileConfig config)
|
||||
{
|
||||
return filters.Aggregate(profile, (current, filter) => filter.Transform(current, config));
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
using Recyclarr.Config.Models;
|
||||
using Recyclarr.TrashGuide.ReleaseProfile;
|
||||
|
||||
namespace Recyclarr.Cli.Pipelines.ReleaseProfile.Filters;
|
||||
|
||||
public class StrictNegativeScoresFilter(ILogger log) : IReleaseProfileFilter
|
||||
{
|
||||
public ReleaseProfileData Transform(ReleaseProfileData profile, ReleaseProfileConfig config)
|
||||
{
|
||||
if (!config.StrictNegativeScores)
|
||||
{
|
||||
return profile;
|
||||
}
|
||||
|
||||
log.Debug("Negative scores will be strictly ignored");
|
||||
var splitPreferred = profile.Preferred.ToLookup(x => x.Score < 0);
|
||||
|
||||
return profile with
|
||||
{
|
||||
Ignored = profile.Ignored.Concat(splitPreferred[true].SelectMany(x => x.Terms)).ToList(),
|
||||
Preferred = splitPreferred[false].ToList()
|
||||
};
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
using Recyclarr.ServarrApi.ReleaseProfile;
|
||||
|
||||
namespace Recyclarr.Cli.Pipelines.ReleaseProfile.Models;
|
||||
|
||||
public record ReleaseProfileTransactionData(
|
||||
IReadOnlyCollection<SonarrReleaseProfile> UpdatedProfiles,
|
||||
IReadOnlyCollection<SonarrReleaseProfile> CreatedProfiles,
|
||||
IReadOnlyCollection<SonarrReleaseProfile> DeletedProfiles
|
||||
);
|
@ -1,14 +0,0 @@
|
||||
using Recyclarr.Cli.Pipelines.Generic;
|
||||
using Recyclarr.Config.Models;
|
||||
using Recyclarr.ServarrApi.ReleaseProfile;
|
||||
|
||||
namespace Recyclarr.Cli.Pipelines.ReleaseProfile.PipelinePhases;
|
||||
|
||||
public class ReleaseProfileApiFetchPhase(IReleaseProfileApiService rpService)
|
||||
: IApiFetchPipelinePhase<ReleaseProfilePipelineContext>
|
||||
{
|
||||
public async Task Execute(ReleaseProfilePipelineContext context, IServiceConfiguration config)
|
||||
{
|
||||
context.ApiFetchOutput = await rpService.GetReleaseProfiles(config);
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
using Recyclarr.Cli.Pipelines.Generic;
|
||||
using Recyclarr.Config.Models;
|
||||
using Recyclarr.ServarrApi.ReleaseProfile;
|
||||
|
||||
namespace Recyclarr.Cli.Pipelines.ReleaseProfile.PipelinePhases;
|
||||
|
||||
public class ReleaseProfileApiPersistencePhase(IReleaseProfileApiService api)
|
||||
: IApiPersistencePipelinePhase<ReleaseProfilePipelineContext>
|
||||
{
|
||||
public async Task Execute(ReleaseProfilePipelineContext context, IServiceConfiguration config)
|
||||
{
|
||||
var transactions = context.TransactionOutput;
|
||||
|
||||
foreach (var profile in transactions.UpdatedProfiles)
|
||||
{
|
||||
await api.UpdateReleaseProfile(config, profile);
|
||||
}
|
||||
|
||||
foreach (var profile in transactions.CreatedProfiles)
|
||||
{
|
||||
await api.CreateReleaseProfile(config, profile);
|
||||
}
|
||||
|
||||
foreach (var profile in transactions.DeletedProfiles)
|
||||
{
|
||||
await api.DeleteReleaseProfile(config, profile.Id);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
using Recyclarr.Cli.Pipelines.Generic;
|
||||
using Recyclarr.Cli.Pipelines.ReleaseProfile.Filters;
|
||||
using Recyclarr.Common.Extensions;
|
||||
using Recyclarr.Config.Models;
|
||||
using Recyclarr.TrashGuide.ReleaseProfile;
|
||||
|
||||
namespace Recyclarr.Cli.Pipelines.ReleaseProfile.PipelinePhases;
|
||||
|
||||
public record ProcessedReleaseProfileData(
|
||||
ReleaseProfileData Profile,
|
||||
IReadOnlyCollection<string> Tags
|
||||
);
|
||||
|
||||
public class ReleaseProfileConfigPhase(
|
||||
ILogger log,
|
||||
IReleaseProfileGuideService guide,
|
||||
IReleaseProfileFilterPipeline filters)
|
||||
: IConfigPipelinePhase<ReleaseProfilePipelineContext>
|
||||
{
|
||||
public Task Execute(ReleaseProfilePipelineContext context, IServiceConfiguration config)
|
||||
{
|
||||
var releaseProfiles = ((SonarrConfiguration) config).ReleaseProfiles;
|
||||
if (!releaseProfiles.Any())
|
||||
{
|
||||
log.Debug("{Instance} has no release profiles", config.InstanceName);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
var profilesFromGuide = guide.GetReleaseProfileData();
|
||||
var filteredProfiles = new List<ProcessedReleaseProfileData>();
|
||||
|
||||
var configProfiles = releaseProfiles.SelectMany(x => x.TrashIds.Select(y => (TrashId: y, Config: x)));
|
||||
foreach (var (trashId, configProfile) in configProfiles)
|
||||
{
|
||||
// For each release profile specified in our YAML config, find the matching profile in the guide.
|
||||
var selectedProfile = profilesFromGuide.FirstOrDefault(x => x.TrashId.EqualsIgnoreCase(trashId));
|
||||
if (selectedProfile is null)
|
||||
{
|
||||
log.Warning("A release profile with Trash ID {TrashId} does not exist", trashId);
|
||||
continue;
|
||||
}
|
||||
|
||||
log.Debug("Found Release Profile: {ProfileName} ({TrashId})", selectedProfile.Name,
|
||||
selectedProfile.TrashId);
|
||||
|
||||
selectedProfile = filters.Process(selectedProfile, configProfile);
|
||||
filteredProfiles.Add(new ProcessedReleaseProfileData(selectedProfile, configProfile.Tags));
|
||||
}
|
||||
|
||||
context.ConfigOutput = filteredProfiles;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
using Recyclarr.Cli.Pipelines.Generic;
|
||||
|
||||
namespace Recyclarr.Cli.Pipelines.ReleaseProfile.PipelinePhases;
|
||||
|
||||
public class ReleaseProfileLogPhase(ILogger log) : ILogPipelinePhase<ReleaseProfilePipelineContext>
|
||||
{
|
||||
public bool LogConfigPhaseAndExitIfNeeded(ReleaseProfilePipelineContext context)
|
||||
{
|
||||
if (context.ConfigOutput is {Count: > 0})
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
log.Debug("No Release Profiles to process");
|
||||
return true;
|
||||
}
|
||||
|
||||
public void LogTransactionNotices(ReleaseProfilePipelineContext context)
|
||||
{
|
||||
}
|
||||
|
||||
public void LogPersistenceResults(ReleaseProfilePipelineContext context)
|
||||
{
|
||||
var transactions = context.TransactionOutput;
|
||||
var somethingChanged = false;
|
||||
|
||||
if (transactions.UpdatedProfiles.Count != 0)
|
||||
{
|
||||
log.Information("Update existing profiles: {ProfileNames}",
|
||||
transactions.UpdatedProfiles.Select(x => x.Name));
|
||||
somethingChanged = true;
|
||||
}
|
||||
|
||||
if (transactions.CreatedProfiles.Count != 0)
|
||||
{
|
||||
log.Information("Create new profiles: {ProfileNames}", transactions.CreatedProfiles.Select(x => x.Name));
|
||||
somethingChanged = true;
|
||||
}
|
||||
|
||||
if (transactions.DeletedProfiles.Count != 0)
|
||||
{
|
||||
log.Information("Deleting old release profiles: {ProfileNames}",
|
||||
transactions.DeletedProfiles.Select(x => x.Name));
|
||||
somethingChanged = true;
|
||||
}
|
||||
|
||||
if (!somethingChanged)
|
||||
{
|
||||
log.Information("All Release Profiles are up to date!");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,86 +0,0 @@
|
||||
using Recyclarr.Cli.Pipelines.Generic;
|
||||
using Recyclarr.ServarrApi.ReleaseProfile;
|
||||
using Spectre.Console;
|
||||
|
||||
namespace Recyclarr.Cli.Pipelines.ReleaseProfile.PipelinePhases;
|
||||
|
||||
public class ReleaseProfilePreviewPhase(IAnsiConsole console) : IPreviewPipelinePhase<ReleaseProfilePipelineContext>
|
||||
{
|
||||
public void Execute(ReleaseProfilePipelineContext context)
|
||||
{
|
||||
var profiles = context.TransactionOutput;
|
||||
|
||||
var tree = new Tree("Release Profiles [red](Preview)[/]");
|
||||
|
||||
PrintCategoryOfChanges("Created Profiles", tree, profiles.CreatedProfiles);
|
||||
PrintCategoryOfChanges("Updated Profiles", tree, profiles.UpdatedProfiles);
|
||||
|
||||
console.WriteLine();
|
||||
console.Write(tree);
|
||||
}
|
||||
|
||||
private void PrintCategoryOfChanges(string nodeTitle, Tree tree, IEnumerable<SonarrReleaseProfile> profiles)
|
||||
{
|
||||
var treeNode = tree.AddNode($"[green]{nodeTitle}[/]");
|
||||
foreach (var profile in profiles)
|
||||
{
|
||||
PrintTermsAndScores(treeNode, profile);
|
||||
}
|
||||
}
|
||||
|
||||
private void PrintTermsAndScores(TreeNode tree, SonarrReleaseProfile profile)
|
||||
{
|
||||
var rpNode = tree.AddNode($"[yellow]{Markup.Escape(profile.Name)}[/]");
|
||||
|
||||
var incPreferred = profile.IncludePreferredWhenRenaming ? "[green]YES[/]" : "[red]NO[/]";
|
||||
rpNode.AddNode($"Include Preferred when Renaming? {incPreferred}");
|
||||
|
||||
PrintTerms(rpNode, "Must Contain", profile.Required);
|
||||
PrintTerms(rpNode, "Must Not Contain", profile.Ignored);
|
||||
PrintPreferredTerms(rpNode, "Preferred", profile.Preferred);
|
||||
|
||||
console.WriteLine("");
|
||||
}
|
||||
|
||||
private static void PrintTerms(TreeNode tree, string title, IReadOnlyCollection<string> terms)
|
||||
{
|
||||
if (terms.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var table = new Table()
|
||||
.AddColumn("[bold]Term[/]");
|
||||
|
||||
foreach (var term in terms)
|
||||
{
|
||||
table.AddRow(Markup.Escape(term));
|
||||
}
|
||||
|
||||
tree.AddNode(title)
|
||||
.AddNode(table);
|
||||
}
|
||||
|
||||
private static void PrintPreferredTerms(
|
||||
TreeNode tree,
|
||||
string title,
|
||||
IReadOnlyCollection<SonarrPreferredTerm> preferredTerms)
|
||||
{
|
||||
if (preferredTerms.Count <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var table = new Table()
|
||||
.AddColumn("[bold]Score[/]")
|
||||
.AddColumn("[bold]Term[/]");
|
||||
|
||||
foreach (var term in preferredTerms)
|
||||
{
|
||||
table.AddRow(term.Score.ToString(), Markup.Escape(term.Term));
|
||||
}
|
||||
|
||||
tree.AddNode(title)
|
||||
.AddNode(table);
|
||||
}
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
using Recyclarr.Cli.Pipelines.Generic;
|
||||
using Recyclarr.Cli.Pipelines.ReleaseProfile.Models;
|
||||
using Recyclarr.Cli.Pipelines.Tags;
|
||||
using Recyclarr.Common.Extensions;
|
||||
using Recyclarr.Config.Models;
|
||||
using Recyclarr.ServarrApi.ReleaseProfile;
|
||||
|
||||
namespace Recyclarr.Cli.Pipelines.ReleaseProfile.PipelinePhases;
|
||||
|
||||
public class ReleaseProfileTransactionPhase(ServiceTagCache tagCache)
|
||||
: ITransactionPipelinePhase<ReleaseProfilePipelineContext>
|
||||
{
|
||||
public void Execute(ReleaseProfilePipelineContext context, IServiceConfiguration config)
|
||||
{
|
||||
var created = new List<SonarrReleaseProfile>();
|
||||
var updated = new List<SonarrReleaseProfile>();
|
||||
|
||||
foreach (var configProfile in context.ConfigOutput)
|
||||
{
|
||||
var title = $"[Trash] {configProfile.Profile.Name}";
|
||||
var matchingServiceProfile = context.ApiFetchOutput.FirstOrDefault(x => x.Name.EqualsIgnoreCase(title));
|
||||
if (matchingServiceProfile is not null)
|
||||
{
|
||||
SetupProfileRequestObject(matchingServiceProfile, configProfile);
|
||||
updated.Add(matchingServiceProfile);
|
||||
}
|
||||
else
|
||||
{
|
||||
var profileToUpdate = new SonarrReleaseProfile {Name = title, Enabled = true};
|
||||
SetupProfileRequestObject(profileToUpdate, configProfile);
|
||||
created.Add(profileToUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
var deleted = DeleteOldManagedProfiles(context.ApiFetchOutput, context.ConfigOutput.AsReadOnly());
|
||||
context.TransactionOutput = new ReleaseProfileTransactionData(updated, created, deleted);
|
||||
}
|
||||
|
||||
private static List<SonarrReleaseProfile> DeleteOldManagedProfiles(
|
||||
IList<SonarrReleaseProfile> serviceData,
|
||||
IReadOnlyList<ProcessedReleaseProfileData> configProfiles)
|
||||
{
|
||||
var profiles = configProfiles.Select(x => x.Profile).ToList();
|
||||
return serviceData
|
||||
.Where(sonarrProfile =>
|
||||
{
|
||||
return sonarrProfile.Name.StartsWithIgnoreCase("[Trash]") &&
|
||||
!profiles.Exists(profile => sonarrProfile.Name.EndsWithIgnoreCase(profile.Name));
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private void SetupProfileRequestObject(SonarrReleaseProfile profileToUpdate, ProcessedReleaseProfileData profile)
|
||||
{
|
||||
profileToUpdate.Preferred = profile.Profile.Preferred
|
||||
.SelectMany(x => x.Terms.Select(termData => new SonarrPreferredTerm(x.Score, termData.Term)))
|
||||
.ToList();
|
||||
|
||||
profileToUpdate.Ignored = profile.Profile.Ignored.Select(x => x.Term).ToList();
|
||||
profileToUpdate.Required = profile.Profile.Required.Select(x => x.Term).ToList();
|
||||
profileToUpdate.IncludePreferredWhenRenaming = profile.Profile.IncludePreferredWhenRenaming;
|
||||
profileToUpdate.Tags = profile.Tags
|
||||
.Select(tagCache.GetTagIdByName)
|
||||
.NotNull()
|
||||
.ToList();
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
using Autofac;
|
||||
using Autofac.Extras.Ordering;
|
||||
using Recyclarr.Cli.Pipelines.ReleaseProfile.Filters;
|
||||
using Recyclarr.Cli.Pipelines.ReleaseProfile.PipelinePhases;
|
||||
using Recyclarr.ServarrApi.ReleaseProfile;
|
||||
|
||||
namespace Recyclarr.Cli.Pipelines.ReleaseProfile;
|
||||
|
||||
public class ReleaseProfileAutofacModule : Module
|
||||
{
|
||||
protected override void Load(ContainerBuilder builder)
|
||||
{
|
||||
base.Load(builder);
|
||||
|
||||
builder.RegisterType<ReleaseProfileApiService>().As<IReleaseProfileApiService>();
|
||||
builder.RegisterType<ReleaseProfileFilterPipeline>().As<IReleaseProfileFilterPipeline>();
|
||||
builder.RegisterType<ReleaseProfileDataLister>();
|
||||
|
||||
// Release Profile Filters (ORDER MATTERS!)
|
||||
builder.RegisterTypes(
|
||||
typeof(IncludeExcludeFilter),
|
||||
typeof(StrictNegativeScoresFilter))
|
||||
.As<IReleaseProfileFilter>()
|
||||
.OrderByRegistration();
|
||||
|
||||
builder.RegisterTypes(
|
||||
typeof(ReleaseProfileConfigPhase),
|
||||
typeof(ReleaseProfilePreviewPhase),
|
||||
typeof(ReleaseProfileApiFetchPhase),
|
||||
typeof(ReleaseProfileTransactionPhase),
|
||||
typeof(ReleaseProfileApiPersistencePhase),
|
||||
typeof(ReleaseProfileLogPhase))
|
||||
.AsImplementedInterfaces();
|
||||
}
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
using System.Text;
|
||||
using Recyclarr.Common.Extensions;
|
||||
using Recyclarr.TrashGuide.ReleaseProfile;
|
||||
using Spectre.Console;
|
||||
|
||||
namespace Recyclarr.Cli.Pipelines.ReleaseProfile;
|
||||
|
||||
public class ReleaseProfileDataLister(IAnsiConsole console, IReleaseProfileGuideService guide)
|
||||
{
|
||||
public void ListReleaseProfiles()
|
||||
{
|
||||
console.WriteLine("\nList of Release Profiles in the TRaSH Guides:\n");
|
||||
|
||||
var profilesFromGuide = guide.GetReleaseProfileData();
|
||||
foreach (var profile in profilesFromGuide)
|
||||
{
|
||||
console.WriteLine($" - {profile.TrashId} # {profile.Name}");
|
||||
}
|
||||
|
||||
console.WriteLine(
|
||||
"\nThe above Release Profiles are in YAML format and ready to be copied & pasted under the `trash_ids:` property.");
|
||||
}
|
||||
|
||||
private static bool HasIdentifiableTerms(ReleaseProfileData profile)
|
||||
{
|
||||
static bool HasTrashIds(IEnumerable<TermData> terms)
|
||||
{
|
||||
return terms.Any(x => !string.IsNullOrEmpty(x.TrashId));
|
||||
}
|
||||
|
||||
return
|
||||
HasTrashIds(profile.Ignored) ||
|
||||
HasTrashIds(profile.Required) ||
|
||||
HasTrashIds(profile.Preferred.SelectMany(x => x.Terms));
|
||||
}
|
||||
|
||||
public void ListTerms(string releaseProfileId)
|
||||
{
|
||||
var profile = guide.GetReleaseProfileData()
|
||||
.FirstOrDefault(x => x.TrashId.EqualsIgnoreCase(releaseProfileId));
|
||||
|
||||
if (profile is null)
|
||||
{
|
||||
throw new ArgumentException("No release profile found with that Trash ID");
|
||||
}
|
||||
|
||||
if (!HasIdentifiableTerms(profile))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"This release profile has no terms that can be filtered " +
|
||||
"(terms must have Trash IDs assigned in order to be filtered)");
|
||||
}
|
||||
|
||||
console.WriteLine();
|
||||
console.WriteLine($"List of Terms for the '{profile.Name}' Release Profile that may be filtered:\n");
|
||||
|
||||
PrintTerms(profile.Required, "Required");
|
||||
PrintTerms(profile.Ignored, "Ignored");
|
||||
PrintTerms(profile.Preferred.SelectMany(x => x.Terms), "Preferred");
|
||||
|
||||
console.WriteLine(
|
||||
"The above Term Filters are in YAML format and ready to be copied & pasted under the `include:` or `exclude:` filter properties.");
|
||||
}
|
||||
|
||||
private void PrintTerms(IEnumerable<TermData> terms, string category)
|
||||
{
|
||||
var filteredTerms = terms.Where(x => x.TrashId.Length != 0).ToList();
|
||||
if (filteredTerms.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
console.WriteLine($"{category} Terms:\n");
|
||||
foreach (var term in filteredTerms)
|
||||
{
|
||||
var line = new StringBuilder($" - {term.TrashId}");
|
||||
if (term.Name.Length != 0)
|
||||
{
|
||||
line.Append($" # {term.Name}");
|
||||
}
|
||||
|
||||
console.WriteLine(line.ToString());
|
||||
}
|
||||
|
||||
console.WriteLine();
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Recyclarr.Cli.Pipelines.Generic;
|
||||
using Recyclarr.Cli.Pipelines.ReleaseProfile.Models;
|
||||
using Recyclarr.Cli.Pipelines.ReleaseProfile.PipelinePhases;
|
||||
using Recyclarr.Common;
|
||||
using Recyclarr.ServarrApi.ReleaseProfile;
|
||||
|
||||
namespace Recyclarr.Cli.Pipelines.ReleaseProfile;
|
||||
|
||||
[SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification =
|
||||
"Context objects are similar to DTOs; for usability we want to assign not append")]
|
||||
public class ReleaseProfilePipelineContext : IPipelineContext
|
||||
{
|
||||
public string PipelineDescription => "Release Profile Pipeline";
|
||||
public IReadOnlyCollection<SupportedServices> SupportedServiceTypes { get; } = new[]
|
||||
{
|
||||
SupportedServices.Sonarr
|
||||
};
|
||||
|
||||
public IList<ProcessedReleaseProfileData> ConfigOutput { get; set; } = default!;
|
||||
public IList<SonarrReleaseProfile> ApiFetchOutput { get; set; } = default!;
|
||||
public ReleaseProfileTransactionData TransactionOutput { get; set; } = default!;
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
using Recyclarr.Cli.Pipelines.Generic;
|
||||
using Recyclarr.Config.Models;
|
||||
using Recyclarr.ServarrApi.Tag;
|
||||
|
||||
namespace Recyclarr.Cli.Pipelines.Tags.PipelinePhases;
|
||||
|
||||
public class TagApiFetchPhase(ISonarrTagApiService api, ServiceTagCache cache)
|
||||
: IApiFetchPipelinePhase<TagPipelineContext>
|
||||
{
|
||||
public async Task Execute(TagPipelineContext context, IServiceConfiguration config)
|
||||
{
|
||||
var tags = await api.GetTags(config);
|
||||
cache.AddTags(tags);
|
||||
context.ApiFetchOutput = tags;
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
using Recyclarr.Cli.Pipelines.Generic;
|
||||
using Recyclarr.Config.Models;
|
||||
using Recyclarr.ServarrApi.Tag;
|
||||
|
||||
namespace Recyclarr.Cli.Pipelines.Tags.PipelinePhases;
|
||||
|
||||
public class TagApiPersistencePhase(ILogger log, ServiceTagCache cache, ISonarrTagApiService api)
|
||||
: IApiPersistencePipelinePhase<TagPipelineContext>
|
||||
{
|
||||
public async Task Execute(TagPipelineContext context, IServiceConfiguration config)
|
||||
{
|
||||
var createdTags = new List<SonarrTag>();
|
||||
foreach (var tag in context.TransactionOutput)
|
||||
{
|
||||
log.Debug("Creating Tag: {Tag}", tag);
|
||||
createdTags.Add(await api.CreateTag(config, tag));
|
||||
}
|
||||
|
||||
cache.AddTags(createdTags);
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
using Recyclarr.Cli.Pipelines.Generic;
|
||||
using Recyclarr.Config.Models;
|
||||
|
||||
namespace Recyclarr.Cli.Pipelines.Tags.PipelinePhases;
|
||||
|
||||
public class TagConfigPhase : IConfigPipelinePhase<TagPipelineContext>
|
||||
{
|
||||
public Task Execute(TagPipelineContext context, IServiceConfiguration config)
|
||||
{
|
||||
context.ConfigOutput = ((SonarrConfiguration) config).ReleaseProfiles
|
||||
.SelectMany(x => x.Tags)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
using Recyclarr.Cli.Pipelines.Generic;
|
||||
|
||||
namespace Recyclarr.Cli.Pipelines.Tags.PipelinePhases;
|
||||
|
||||
public class TagLogPhase(ILogger log) : ILogPipelinePhase<TagPipelineContext>
|
||||
{
|
||||
public bool LogConfigPhaseAndExitIfNeeded(TagPipelineContext context)
|
||||
{
|
||||
if (!context.ConfigOutput.Any())
|
||||
{
|
||||
log.Debug("No tags to process");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void LogTransactionNotices(TagPipelineContext context)
|
||||
{
|
||||
}
|
||||
|
||||
public void LogPersistenceResults(TagPipelineContext context)
|
||||
{
|
||||
if (context.TransactionOutput.Any())
|
||||
{
|
||||
log.Information("Created {Count} Tags: {Tags}",
|
||||
context.TransactionOutput.Count,
|
||||
context.TransactionOutput);
|
||||
}
|
||||
else
|
||||
{
|
||||
log.Information("All tags are up to date!");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
using Castle.Core.Internal;
|
||||
using Recyclarr.Cli.Pipelines.Generic;
|
||||
using Spectre.Console;
|
||||
|
||||
namespace Recyclarr.Cli.Pipelines.Tags.PipelinePhases;
|
||||
|
||||
public class TagPreviewPhase(IAnsiConsole console) : IPreviewPipelinePhase<TagPipelineContext>
|
||||
{
|
||||
public void Execute(TagPipelineContext context)
|
||||
{
|
||||
var tagsToCreate = context.TransactionOutput;
|
||||
|
||||
if (tagsToCreate.IsNullOrEmpty())
|
||||
{
|
||||
console.WriteLine();
|
||||
console.MarkupLine("[green]No tags to create[/]");
|
||||
console.WriteLine();
|
||||
return;
|
||||
}
|
||||
|
||||
var table = new Table {Border = TableBorder.Simple};
|
||||
table.AddColumn("[olive]Tags To Create[/]");
|
||||
|
||||
foreach (var tag in tagsToCreate)
|
||||
{
|
||||
table.AddRow(tag);
|
||||
}
|
||||
|
||||
console.Write(table);
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
using Recyclarr.Cli.Pipelines.Generic;
|
||||
using Recyclarr.Common.Extensions;
|
||||
using Recyclarr.Config.Models;
|
||||
|
||||
namespace Recyclarr.Cli.Pipelines.Tags.PipelinePhases;
|
||||
|
||||
public class TagTransactionPhase : ITransactionPipelinePhase<TagPipelineContext>
|
||||
{
|
||||
public void Execute(TagPipelineContext context, IServiceConfiguration config)
|
||||
{
|
||||
// List of tags in config that do not already exist in the service. The goal is to figure out which tags need to
|
||||
// be created.
|
||||
context.TransactionOutput = context.ConfigOutput
|
||||
.Where(ct => context.ApiFetchOutput.All(st => !st.Label.EqualsIgnoreCase(ct)))
|
||||
.ToList();
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
using Recyclarr.Common.Extensions;
|
||||
using Recyclarr.ServarrApi.Tag;
|
||||
|
||||
namespace Recyclarr.Cli.Pipelines.Tags;
|
||||
|
||||
public class ServiceTagCache : IPipelineCache
|
||||
{
|
||||
private readonly HashSet<SonarrTag> _serviceTags = new();
|
||||
|
||||
public IEnumerable<SonarrTag> Tags => _serviceTags;
|
||||
|
||||
public void AddTags(IEnumerable<SonarrTag> tags)
|
||||
{
|
||||
_serviceTags.AddRange(tags);
|
||||
}
|
||||
|
||||
public int? GetTagIdByName(string name)
|
||||
{
|
||||
var foundTag = _serviceTags.FirstOrDefault(x => x.Label.EqualsIgnoreCase(name));
|
||||
return foundTag?.Id;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_serviceTags.Clear();
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Recyclarr.Cli.Pipelines.Generic;
|
||||
using Recyclarr.Common;
|
||||
using Recyclarr.ServarrApi.Tag;
|
||||
|
||||
namespace Recyclarr.Cli.Pipelines.Tags;
|
||||
|
||||
[SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification =
|
||||
"Context objects are similar to DTOs; for usability we want to assign not append")]
|
||||
public class TagPipelineContext : IPipelineContext
|
||||
{
|
||||
public string PipelineDescription => "Tag Pipeline";
|
||||
public IReadOnlyCollection<SupportedServices> SupportedServiceTypes { get; } = new[]
|
||||
{
|
||||
SupportedServices.Sonarr
|
||||
};
|
||||
|
||||
public IList<string> ConfigOutput { get; set; } = default!;
|
||||
public IList<SonarrTag> ApiFetchOutput { get; set; } = default!;
|
||||
public IList<string> TransactionOutput { get; set; } = default!;
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
using Autofac;
|
||||
using Recyclarr.Cli.Pipelines.Tags.PipelinePhases;
|
||||
|
||||
namespace Recyclarr.Cli.Pipelines.Tags;
|
||||
|
||||
public class TagsAutofacModule : Module
|
||||
{
|
||||
protected override void Load(ContainerBuilder builder)
|
||||
{
|
||||
base.Load(builder);
|
||||
|
||||
builder.RegisterType<ServiceTagCache>().As<IPipelineCache>()
|
||||
.AsSelf()
|
||||
.InstancePerLifetimeScope();
|
||||
|
||||
builder.RegisterTypes(
|
||||
typeof(TagConfigPhase),
|
||||
typeof(TagPreviewPhase),
|
||||
typeof(TagApiFetchPhase),
|
||||
typeof(TagTransactionPhase),
|
||||
typeof(TagApiPersistencePhase),
|
||||
typeof(TagLogPhase))
|
||||
.AsImplementedInterfaces();
|
||||
}
|
||||
}
|
@ -1,75 +1,41 @@
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/recyclarr/recyclarr/master/schemas/config-schema.json
|
||||
|
||||
# A starter config to use with Recyclarr. Most values are set to "reasonable defaults". Update the
|
||||
# values below as needed for your instance. You will be required to update the API Key and URL for
|
||||
# each instance you want to use.
|
||||
# An empty starter config to use with Recyclarr. Update the values below as needed for your
|
||||
# instance. You will be required to update the `api_key` and `base_url` for each instance you want
|
||||
# to use.
|
||||
#
|
||||
# Many optional settings have been omitted to keep this template simple. Note that there's no "one
|
||||
# size fits all" configuration. Please refer to the guide to understand how to build the appropriate
|
||||
# configuration based on your hardware setup and capabilities.
|
||||
# If you'd rather use pre-built configuration instead of building your own from scratch, see these
|
||||
# pages:
|
||||
# - Config Templates: https://recyclarr.dev/wiki/guide-configs/
|
||||
# - CLI Command: http://recyclarr.dev/wiki/cli/config/list/templates/
|
||||
#
|
||||
# For any lines that mention uncommenting YAML, you simply need to remove the leading hash (`#`).
|
||||
# The YAML comments will already be at the appropriate indentation.
|
||||
# This file WILL NOT WORK as it currently is. You need to read the documentation and build this
|
||||
# configuration from scratch. Note that there's no "one size fits all" configuration. Please refer
|
||||
# to the TRaSH Guides to understand how to build the appropriate configuration based on your
|
||||
# hardware setup and capabilities.
|
||||
#
|
||||
# For more details on the configuration, see the Configuration Reference on the wiki here:
|
||||
# https://recyclarr.dev/wiki/yaml/config-reference/
|
||||
#
|
||||
# Want a more flexible file layout?
|
||||
# See: https://recyclarr.dev/wiki/file-structure/
|
||||
|
||||
# Configuration specific to Sonarr
|
||||
# Configuration specific to Sonarr. For Radarr, the layout is the same.
|
||||
# See: http://recyclarr.dev/wiki/yaml/config-reference/basic/
|
||||
sonarr:
|
||||
series:
|
||||
# Set the URL/API Key to your actual instance
|
||||
base_url: http://localhost:8989
|
||||
api_key: YOUR_KEY_HERE
|
||||
|
||||
# Quality definitions from the guide to sync to Sonarr. Choices: series, anime
|
||||
quality_definition:
|
||||
type: series
|
||||
|
||||
# Release profiles from the guide to sync to Sonarr v3 (Sonarr v4 does not use this!)
|
||||
# Use `recyclarr list release-profiles` for values you can put here.
|
||||
# https://trash-guides.info/Sonarr/Sonarr-Release-Profile-RegEx/
|
||||
release_profiles:
|
||||
# Series
|
||||
- trash_ids:
|
||||
- EBC725268D687D588A20CBC5F97E538B # Low Quality Groups
|
||||
- 1B018E0C53EC825085DD911102E2CA36 # Release Sources (Streaming Service)
|
||||
- 71899E6C303A07AF0E4746EFF9873532 # P2P Groups + Repack/Proper
|
||||
# Anime (Uncomment below if you want it)
|
||||
#- trash_ids:
|
||||
# - d428eda85af1df8904b4bbe4fc2f537c # Anime - First release profile
|
||||
# - 6cd9e10bb5bb4c63d2d7cd3279924c7b # Anime - Second release profile
|
||||
|
||||
# Configuration specific to Radarr.
|
||||
radarr:
|
||||
movies:
|
||||
# Set the URL/API Key to your actual instance
|
||||
base_url: http://localhost:7878
|
||||
api_key: YOUR_KEY_HERE
|
||||
|
||||
# Which quality definition in the guide to sync to Radarr. Only choice right now is 'movie'
|
||||
# See: https://recyclarr.dev/wiki/yaml/config-reference/quality-definition/
|
||||
quality_definition:
|
||||
type: movie
|
||||
|
||||
# Set to 'true' to automatically remove custom formats from Radarr when they are removed from
|
||||
# the guide or your configuration. This will NEVER delete custom formats you manually created!
|
||||
delete_old_custom_formats: false
|
||||
# See: http://localhost:3000/wiki/yaml/config-reference/quality-profiles/
|
||||
quality_profiles:
|
||||
|
||||
# See: http://localhost:3000/wiki/yaml/config-reference/custom-formats/
|
||||
custom_formats:
|
||||
# A list of custom formats to sync to Radarr.
|
||||
# Use `recyclarr list custom-formats radarr` for values you can put here.
|
||||
# https://trash-guides.info/Radarr/Radarr-collection-of-custom-formats/
|
||||
- trash_ids:
|
||||
- ed38b889b31be83fda192888e2286d83 # BR-DISK
|
||||
- 90cedc1fea7ea5d11298bebd3d1d3223 # EVO (no WEBDL)
|
||||
- 90a6f9a284dff5103f6346090e6280c8 # LQ
|
||||
- dc98083864ea246d05a42df0d05f81cc # x265 (720/1080p)
|
||||
- b8cd450cbfa689c0259a01d9e29ba3d6 # 3D
|
||||
|
||||
# Uncomment the below properties to specify one or more quality profiles that should be
|
||||
# updated with scores from the guide for each custom format. Without this, custom formats
|
||||
# are synced to Radarr but no scores are set in any quality profiles.
|
||||
#quality_profiles:
|
||||
# - name: Quality Profile 1
|
||||
# - name: Quality Profile 2
|
||||
# #score: -9999 # Optional score to assign to all CFs. Overrides scores in the guide.
|
||||
# #reset_unmatched_scores: true # Optionally set other scores to 0 if they are not listed in 'names' above.
|
||||
# See: http://localhost:3000/wiki/yaml/config-reference/media-naming/
|
||||
media_naming:
|
||||
|
@ -1,11 +0,0 @@
|
||||
using Recyclarr.Config.Models;
|
||||
|
||||
namespace Recyclarr.ServarrApi.ReleaseProfile;
|
||||
|
||||
public interface IReleaseProfileApiService
|
||||
{
|
||||
Task UpdateReleaseProfile(IServiceConfiguration config, SonarrReleaseProfile profile);
|
||||
Task<SonarrReleaseProfile> CreateReleaseProfile(IServiceConfiguration config, SonarrReleaseProfile profile);
|
||||
Task<IList<SonarrReleaseProfile>> GetReleaseProfiles(IServiceConfiguration config);
|
||||
Task DeleteReleaseProfile(IServiceConfiguration config, int releaseProfileId);
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
using Flurl.Http;
|
||||
using Recyclarr.Config.Models;
|
||||
using Recyclarr.ServarrApi.Http.Servarr;
|
||||
|
||||
namespace Recyclarr.ServarrApi.ReleaseProfile;
|
||||
|
||||
public class ReleaseProfileApiService(IServarrRequestBuilder service) : IReleaseProfileApiService
|
||||
{
|
||||
public async Task UpdateReleaseProfile(IServiceConfiguration config, SonarrReleaseProfile profile)
|
||||
{
|
||||
await service.Request(config, "releaseprofile", profile.Id)
|
||||
.PutJsonAsync(profile);
|
||||
}
|
||||
|
||||
public async Task<SonarrReleaseProfile> CreateReleaseProfile(
|
||||
IServiceConfiguration config,
|
||||
SonarrReleaseProfile profile)
|
||||
{
|
||||
return await service.Request(config, "releaseprofile")
|
||||
.PostJsonAsync(profile)
|
||||
.ReceiveJson<SonarrReleaseProfile>();
|
||||
}
|
||||
|
||||
public async Task<IList<SonarrReleaseProfile>> GetReleaseProfiles(IServiceConfiguration config)
|
||||
{
|
||||
return await service.Request(config, "releaseprofile")
|
||||
.GetJsonAsync<List<SonarrReleaseProfile>>();
|
||||
}
|
||||
|
||||
public async Task DeleteReleaseProfile(IServiceConfiguration config, int releaseProfileId)
|
||||
{
|
||||
await service.Request(config, "releaseprofile", releaseProfileId)
|
||||
.DeleteAsync();
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Recyclarr.ServarrApi.ReleaseProfile;
|
||||
|
||||
[UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.Members)]
|
||||
public class SonarrPreferredTerm(int score, string term)
|
||||
{
|
||||
[JsonPropertyName("key")]
|
||||
public string Term { get; set; } = term;
|
||||
|
||||
[JsonPropertyName("value")]
|
||||
public int Score { get; set; } = score;
|
||||
}
|
||||
|
||||
[UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.Members)]
|
||||
public class SonarrReleaseProfile
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
public string Name { get; set; } = "";
|
||||
public IReadOnlyCollection<string> Required { get; set; } = new List<string>();
|
||||
public IReadOnlyCollection<string> Ignored { get; set; } = new List<string>();
|
||||
public IReadOnlyCollection<SonarrPreferredTerm> Preferred { get; set; } = new List<SonarrPreferredTerm>();
|
||||
public bool IncludePreferredWhenRenaming { get; set; }
|
||||
public int IndexerId { get; set; }
|
||||
public IReadOnlyCollection<int> Tags { get; set; } = new List<int>();
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
using Recyclarr.Config.Models;
|
||||
|
||||
namespace Recyclarr.ServarrApi.Tag;
|
||||
|
||||
public interface ISonarrTagApiService
|
||||
{
|
||||
Task<IList<SonarrTag>> GetTags(IServiceConfiguration config);
|
||||
Task<SonarrTag> CreateTag(IServiceConfiguration config, string tag);
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
using JetBrains.Annotations;
|
||||
using Recyclarr.Common;
|
||||
|
||||
namespace Recyclarr.ServarrApi.Tag;
|
||||
|
||||
public class SonarrTag
|
||||
{
|
||||
public static IEqualityComparer<SonarrTag> Comparer { get; } =
|
||||
new GenericEqualityComparer<SonarrTag>((x, y) => x.Id == y.Id, x => x.Id);
|
||||
|
||||
public string Label { get; [UsedImplicitly] set; } = "";
|
||||
public int Id { get; [UsedImplicitly] set; }
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
using Flurl.Http;
|
||||
using Recyclarr.Config.Models;
|
||||
using Recyclarr.ServarrApi.Http.Servarr;
|
||||
|
||||
namespace Recyclarr.ServarrApi.Tag;
|
||||
|
||||
public class SonarrTagApiService(IServarrRequestBuilder service) : ISonarrTagApiService
|
||||
{
|
||||
public async Task<IList<SonarrTag>> GetTags(IServiceConfiguration config)
|
||||
{
|
||||
return await service.Request(config, "tag")
|
||||
.GetJsonAsync<List<SonarrTag>>();
|
||||
}
|
||||
|
||||
public async Task<SonarrTag> CreateTag(IServiceConfiguration config, string tag)
|
||||
{
|
||||
return await service.Request(config, "tag")
|
||||
.PostJsonAsync(new {label = tag})
|
||||
.ReceiveJson<SonarrTag>();
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
namespace Recyclarr.TrashGuide.ReleaseProfile;
|
||||
|
||||
public interface IReleaseProfileGuideService
|
||||
{
|
||||
IReadOnlyList<ReleaseProfileData> GetReleaseProfileData();
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Recyclarr.TrashGuide.ReleaseProfile;
|
||||
|
||||
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
|
||||
public record TermData
|
||||
{
|
||||
[JsonPropertyName("trash_id")]
|
||||
public string TrashId { get; init; } = string.Empty;
|
||||
|
||||
public string Name { get; init; } = string.Empty;
|
||||
public string Term { get; init; } = string.Empty;
|
||||
|
||||
public sealed override string ToString()
|
||||
{
|
||||
return $"[TrashId: {TrashId}] [Name: {Name}] [Term: {Term}]";
|
||||
}
|
||||
}
|
||||
|
||||
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
|
||||
public record PreferredTermData
|
||||
{
|
||||
public int Score { get; init; }
|
||||
public IReadOnlyCollection<TermData> Terms { get; init; } = Array.Empty<TermData>();
|
||||
|
||||
public void Deconstruct(out int score, out IReadOnlyCollection<TermData> terms)
|
||||
{
|
||||
score = Score;
|
||||
terms = Terms;
|
||||
}
|
||||
|
||||
public sealed override string ToString()
|
||||
{
|
||||
return $"[Score: {Score}] [Terms: {Terms.Count}]";
|
||||
}
|
||||
}
|
||||
|
||||
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
|
||||
public record ReleaseProfileData
|
||||
{
|
||||
[JsonPropertyName("trash_id")]
|
||||
public string TrashId { get; init; } = string.Empty;
|
||||
|
||||
public string Name { get; init; } = string.Empty;
|
||||
public bool IncludePreferredWhenRenaming { get; init; }
|
||||
public IReadOnlyCollection<TermData> Required { get; init; } = Array.Empty<TermData>();
|
||||
public IReadOnlyCollection<TermData> Ignored { get; init; } = Array.Empty<TermData>();
|
||||
public IReadOnlyCollection<PreferredTermData> Preferred { get; init; } = Array.Empty<PreferredTermData>();
|
||||
|
||||
public sealed override string ToString()
|
||||
{
|
||||
return $"[TrashId: {TrashId}] " +
|
||||
$"[Name: {Name}] " +
|
||||
$"[IncludePreferred: {IncludePreferredWhenRenaming}] " +
|
||||
$"[Required: {Required.Count}] " +
|
||||
$"[Ignored: {Ignored.Count}] " +
|
||||
$"[Preferred: {Preferred.Count}]";
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
using FluentValidation.Results;
|
||||
using Recyclarr.Common.FluentValidation;
|
||||
|
||||
namespace Recyclarr.TrashGuide.ReleaseProfile;
|
||||
|
||||
public class ReleaseProfileDataValidationFilterer(ILogger log)
|
||||
{
|
||||
private void LogInvalidTerm(IReadOnlyCollection<ValidationFailure> failures, string filterDescription)
|
||||
{
|
||||
log.Debug("Validation failed on term data ({Filter}): {Failures}", filterDescription, failures);
|
||||
}
|
||||
|
||||
public IEnumerable<TermData> FilterTerms(IEnumerable<TermData> terms)
|
||||
{
|
||||
return terms.IsValid(new TermDataValidator(), (e, x) => LogInvalidTerm(e, x.ToString()));
|
||||
}
|
||||
|
||||
public IEnumerable<PreferredTermData> FilterTerms(IEnumerable<PreferredTermData> terms)
|
||||
{
|
||||
return terms.IsValid(new PreferredTermDataValidator(), (e, x) => LogInvalidTerm(e, x.ToString()));
|
||||
}
|
||||
|
||||
private ReleaseProfileData FilterProfile(ReleaseProfileData profile)
|
||||
{
|
||||
return profile with
|
||||
{
|
||||
Required = FilterTerms(profile.Required).ToList(),
|
||||
Ignored = FilterTerms(profile.Ignored).ToList(),
|
||||
Preferred = FilterTerms(profile.Preferred).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
public IEnumerable<ReleaseProfileData> FilterProfiles(IEnumerable<ReleaseProfileData> data)
|
||||
{
|
||||
return data
|
||||
.Select(FilterProfile)
|
||||
.IsValid(new ReleaseProfileDataValidator(), (e, x) =>
|
||||
{
|
||||
log.Warning("Excluding invalid release profile: {Profile}", x.ToString());
|
||||
log.Debug("Release profile excluded for these reasons: {Reasons}", e);
|
||||
});
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace Recyclarr.TrashGuide.ReleaseProfile;
|
||||
|
||||
public class TermDataValidator : AbstractValidator<TermData>
|
||||
{
|
||||
public TermDataValidator()
|
||||
{
|
||||
RuleFor(x => x.Term).NotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public class PreferredTermDataValidator : AbstractValidator<PreferredTermData>
|
||||
{
|
||||
public PreferredTermDataValidator()
|
||||
{
|
||||
RuleFor(x => x.Terms).NotEmpty();
|
||||
RuleForEach(x => x.Terms).SetValidator(new TermDataValidator());
|
||||
}
|
||||
}
|
||||
|
||||
public class ReleaseProfileDataValidator : AbstractValidator<ReleaseProfileData>
|
||||
{
|
||||
public ReleaseProfileDataValidator()
|
||||
{
|
||||
RuleFor(x => x.Name).NotEmpty();
|
||||
RuleFor(x => x.TrashId).NotEmpty();
|
||||
RuleFor(x => x)
|
||||
.Must(x => x.Required.Count != 0 || x.Ignored.Count != 0 || x.Preferred.Count != 0)
|
||||
.WithMessage("Must have at least one of Required, Ignored, or Preferred terms");
|
||||
}
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
using System.IO.Abstractions;
|
||||
using System.Text.Json;
|
||||
using Recyclarr.Json;
|
||||
|
||||
namespace Recyclarr.TrashGuide.ReleaseProfile;
|
||||
|
||||
public class ReleaseProfileGuideParser(ILogger log)
|
||||
{
|
||||
private readonly JsonSerializerOptions _jsonSettings = new(GlobalJsonSerializerSettings.Services)
|
||||
{
|
||||
Converters =
|
||||
{
|
||||
new CollectionJsonConverter(),
|
||||
new TermDataConverter()
|
||||
}
|
||||
};
|
||||
|
||||
private async Task<ReleaseProfileData?> LoadAndParseFile(IFileInfo file)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var stream = file.OpenRead();
|
||||
return await JsonSerializer.DeserializeAsync<ReleaseProfileData>(stream, _jsonSettings);
|
||||
}
|
||||
catch (JsonException e)
|
||||
{
|
||||
HandleJsonException(e, file);
|
||||
}
|
||||
catch (AggregateException ae) when (ae.InnerException is JsonException e)
|
||||
{
|
||||
HandleJsonException(e, file);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void HandleJsonException(JsonException exception, IFileInfo file)
|
||||
{
|
||||
log.Warning(exception,
|
||||
"Failed to parse Sonarr JSON file (This likely indicates a bug that should be " +
|
||||
"reported in the TRaSH repo): {File}", file.Name);
|
||||
}
|
||||
|
||||
public IEnumerable<ReleaseProfileData> GetReleaseProfileData(IEnumerable<IDirectoryInfo> paths)
|
||||
{
|
||||
var tasks = JsonUtils.GetJsonFilesInDirectories(paths, log).Select(LoadAndParseFile);
|
||||
var data = Task.WhenAll(tasks).Result
|
||||
// Make non-nullable type and filter out null values
|
||||
.Choose(x => x is not null ? (true, x) : default);
|
||||
|
||||
var validator = new ReleaseProfileDataValidationFilterer(log);
|
||||
return validator.FilterProfiles(data);
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
using Recyclarr.Repo;
|
||||
|
||||
namespace Recyclarr.TrashGuide.ReleaseProfile;
|
||||
|
||||
public class ReleaseProfileGuideService : IReleaseProfileGuideService
|
||||
{
|
||||
private readonly Lazy<IReadOnlyList<ReleaseProfileData>> _guideData;
|
||||
|
||||
public ReleaseProfileGuideService(IRepoMetadataBuilder metadataBuilder, ReleaseProfileGuideParser parser)
|
||||
{
|
||||
_guideData = new Lazy<IReadOnlyList<ReleaseProfileData>>(() =>
|
||||
{
|
||||
var metadata = metadataBuilder.GetMetadata();
|
||||
var paths = metadataBuilder.ToDirectoryInfoList(metadata.JsonPaths.Sonarr.ReleaseProfiles);
|
||||
return parser.GetReleaseProfileData(paths).ToList();
|
||||
});
|
||||
}
|
||||
|
||||
public IReadOnlyList<ReleaseProfileData> GetReleaseProfileData()
|
||||
{
|
||||
return _guideData.Value;
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Recyclarr.Json;
|
||||
|
||||
namespace Recyclarr.TrashGuide.ReleaseProfile;
|
||||
|
||||
public class TermDataConverter : JsonConverter<TermData>
|
||||
{
|
||||
public override TermData? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType is JsonTokenType.String)
|
||||
{
|
||||
var str = reader.GetString();
|
||||
return str is not null ? new TermData {Term = str} : null;
|
||||
}
|
||||
|
||||
return JsonSerializer.Deserialize<TermData>(ref reader, options.CopyOptionsWithout<TermDataConverter>());
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, TermData value, JsonSerializerOptions options)
|
||||
{
|
||||
JsonSerializer.Serialize(writer, value, options.CopyOptionsWithout<TermDataConverter>());
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
# First Release Profile
|
||||
|
||||
Do check mark include preferred when renaming
|
||||
|
||||
This score is negative [-1]
|
||||
|
||||
```
|
||||
abc
|
||||
```
|
||||
|
||||
# Second Release Profile
|
||||
|
||||
Do not check mark include preferred when renaming
|
||||
|
||||
This score is positive [1]
|
||||
|
||||
```
|
||||
xyz
|
||||
```
|
@ -1,13 +0,0 @@
|
||||
# Test Release Profile
|
||||
|
||||
This score is negative [-1]
|
||||
|
||||
```
|
||||
abc
|
||||
```
|
||||
|
||||
This score is positive [0]
|
||||
|
||||
```
|
||||
xyz
|
||||
```
|
@ -1,22 +0,0 @@
|
||||
### Release Profile 1
|
||||
|
||||
The score is [100]
|
||||
|
||||
```
|
||||
term1
|
||||
```
|
||||
|
||||
This is another Score that should not be used [200]
|
||||
|
||||
#### Must not contain
|
||||
|
||||
```
|
||||
term2
|
||||
term3
|
||||
```
|
||||
|
||||
#### Must contain
|
||||
|
||||
```
|
||||
term4
|
||||
```
|
@ -1,207 +0,0 @@
|
||||
using Recyclarr.Cli.Pipelines.ReleaseProfile.Filters;
|
||||
using Recyclarr.Config.Models;
|
||||
using Recyclarr.TrashGuide.ReleaseProfile;
|
||||
|
||||
namespace Recyclarr.Cli.Tests.Pipelines.ReleaseProfile.Filters;
|
||||
|
||||
[TestFixture]
|
||||
public class ReleaseProfileDataFiltererTest
|
||||
{
|
||||
[Test, AutoMockData]
|
||||
public void Include_terms_filter_works(ReleaseProfileDataFilterer sut)
|
||||
{
|
||||
var filter = new[] {"1", "2"};
|
||||
var terms = new TermData[]
|
||||
{
|
||||
new() {TrashId = "1", Term = "term1"},
|
||||
new() {TrashId = "2", Term = "term2"},
|
||||
new() {TrashId = "3", Term = "term3"},
|
||||
new() {Term = "term4"},
|
||||
new() {Term = "term5"}
|
||||
};
|
||||
|
||||
var result = sut.IncludeTerms(terms, filter);
|
||||
|
||||
result.Should().BeEquivalentTo(new TermData[]
|
||||
{
|
||||
new() {TrashId = "1", Term = "term1"},
|
||||
new() {TrashId = "2", Term = "term2"}
|
||||
});
|
||||
}
|
||||
|
||||
[Test, AutoMockData]
|
||||
public void Include_preferred_terms_filter_works(ReleaseProfileDataFilterer sut)
|
||||
{
|
||||
var filter = new[] {"1", "2"};
|
||||
var terms = new PreferredTermData[]
|
||||
{
|
||||
new()
|
||||
{
|
||||
Score = 10, Terms = new TermData[]
|
||||
{
|
||||
new() {TrashId = "1", Term = "term1"},
|
||||
new() {TrashId = "2", Term = "term2"},
|
||||
new() {TrashId = "3", Term = "term3"},
|
||||
new() {Term = "term4"},
|
||||
new() {Term = "term5"}
|
||||
}
|
||||
},
|
||||
new()
|
||||
{
|
||||
Score = 20, Terms = new TermData[]
|
||||
{
|
||||
new() {TrashId = "4", Term = "term4"}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var result = sut.IncludeTerms(terms, filter);
|
||||
|
||||
result.Should().BeEquivalentTo(new PreferredTermData[]
|
||||
{
|
||||
new()
|
||||
{
|
||||
Score = 10, Terms = new TermData[]
|
||||
{
|
||||
new() {TrashId = "1", Term = "term1"},
|
||||
new() {TrashId = "2", Term = "term2"}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test, AutoMockData]
|
||||
public void Exclude_terms_filter_works(ReleaseProfileDataFilterer sut)
|
||||
{
|
||||
var filter = new[] {"1", "2"};
|
||||
var terms = new TermData[]
|
||||
{
|
||||
new() {TrashId = "1", Term = "term1"},
|
||||
new() {TrashId = "2", Term = "term2"},
|
||||
new() {TrashId = "3", Term = "term3"},
|
||||
new() {Term = "term4"},
|
||||
new() {Term = "term5"}
|
||||
};
|
||||
|
||||
var result = sut.ExcludeTerms(terms, filter);
|
||||
|
||||
result.Should().BeEquivalentTo(new TermData[]
|
||||
{
|
||||
new() {TrashId = "3", Term = "term3"},
|
||||
new() {Term = "term4"},
|
||||
new() {Term = "term5"}
|
||||
});
|
||||
}
|
||||
|
||||
[Test, AutoMockData]
|
||||
public void Exclude_preferred_terms_filter_works(ReleaseProfileDataFilterer sut)
|
||||
{
|
||||
var filter = new[] {"1", "2"};
|
||||
var terms = new PreferredTermData[]
|
||||
{
|
||||
new()
|
||||
{
|
||||
Score = 10,
|
||||
Terms = new TermData[]
|
||||
{
|
||||
new() {TrashId = "1", Term = "term1"},
|
||||
new() {TrashId = "2", Term = "term2"},
|
||||
new() {TrashId = "3", Term = "term3"},
|
||||
new() {Term = "term4"},
|
||||
new() {Term = "term5"}
|
||||
}
|
||||
},
|
||||
new()
|
||||
{
|
||||
Score = 20,
|
||||
Terms = new TermData[]
|
||||
{
|
||||
new() {TrashId = "4", Term = "term4"},
|
||||
new() {Term = "term6"},
|
||||
new() {Term = "term7"}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var result = sut.ExcludeTerms(terms, filter);
|
||||
|
||||
result.Should().BeEquivalentTo(new PreferredTermData[]
|
||||
{
|
||||
new()
|
||||
{
|
||||
Score = 10,
|
||||
Terms = new TermData[]
|
||||
{
|
||||
new() {TrashId = "3", Term = "term3"},
|
||||
new() {Term = "term4"},
|
||||
new() {Term = "term5"}
|
||||
}
|
||||
},
|
||||
new()
|
||||
{
|
||||
Score = 20,
|
||||
Terms = new TermData[]
|
||||
{
|
||||
new() {TrashId = "4", Term = "term4"},
|
||||
new() {Term = "term6"},
|
||||
new() {Term = "term7"}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test, AutoMockData]
|
||||
public void Filter_profile_data_with_invalid_terms(ReleaseProfileDataFilterer sut)
|
||||
{
|
||||
var profileData = new ReleaseProfileData
|
||||
{
|
||||
Preferred = new PreferredTermData[]
|
||||
{
|
||||
new()
|
||||
{
|
||||
Score = 10, Terms = new TermData[]
|
||||
{
|
||||
new() {TrashId = "1", Term = "term1"}, // excluded by filter
|
||||
new() {TrashId = "2", Term = ""}, // excluded because it's invalid
|
||||
new() {TrashId = "3", Term = "term3"}
|
||||
}
|
||||
},
|
||||
new()
|
||||
{
|
||||
Score = 20, Terms = new TermData[]
|
||||
{
|
||||
new() {TrashId = "4", Term = "term4"}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var filter = new SonarrProfileFilterConfig
|
||||
{
|
||||
Exclude = new[] {"1"}
|
||||
};
|
||||
|
||||
var result = sut.FilterProfile(profileData, filter);
|
||||
|
||||
result.Should().BeEquivalentTo(new ReleaseProfileData
|
||||
{
|
||||
Preferred = new PreferredTermData[]
|
||||
{
|
||||
new()
|
||||
{
|
||||
Score = 10, Terms = new TermData[]
|
||||
{
|
||||
new() {TrashId = "3", Term = "term3"}
|
||||
}
|
||||
},
|
||||
new()
|
||||
{
|
||||
Score = 20, Terms = new TermData[]
|
||||
{
|
||||
new() {TrashId = "4", Term = "term4"}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
using Recyclarr.Cli.Pipelines.ReleaseProfile.Filters;
|
||||
using Recyclarr.Config.Models;
|
||||
using Recyclarr.TrashGuide.ReleaseProfile;
|
||||
|
||||
namespace Recyclarr.Cli.Tests.Pipelines.ReleaseProfile.Filters;
|
||||
|
||||
[TestFixture]
|
||||
public class StrictNegativeScoresFilterTest
|
||||
{
|
||||
private static readonly ReleaseProfileData TestProfile = new()
|
||||
{
|
||||
Preferred = new[]
|
||||
{
|
||||
new PreferredTermData
|
||||
{
|
||||
Score = -1,
|
||||
Terms = new[]
|
||||
{
|
||||
new TermData
|
||||
{
|
||||
TrashId = "abc",
|
||||
Term = "a"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
[Test, AutoMockData]
|
||||
public void Preferred_with_negative_scores_is_treated_as_ignored_when_strict_negative_scores_enabled(
|
||||
StrictNegativeScoresFilter sut)
|
||||
{
|
||||
var config = new ReleaseProfileConfig
|
||||
{
|
||||
StrictNegativeScores = true
|
||||
};
|
||||
|
||||
var result = sut.Transform(TestProfile, config);
|
||||
|
||||
result.Preferred.Should().BeEmpty();
|
||||
result.Ignored.Should().BeEquivalentTo(TestProfile.Preferred.First().Terms);
|
||||
}
|
||||
|
||||
[Test, AutoMockData]
|
||||
public void Preferred_and_ignored_untouched_when_strict_negative_scores_disabled(StrictNegativeScoresFilter sut)
|
||||
{
|
||||
var config = new ReleaseProfileConfig
|
||||
{
|
||||
StrictNegativeScores = false
|
||||
};
|
||||
|
||||
var result = sut.Transform(TestProfile, config);
|
||||
result.Should().BeSameAs(TestProfile);
|
||||
}
|
||||
}
|
@ -1,80 +0,0 @@
|
||||
using Recyclarr.Cli.Pipelines.ReleaseProfile;
|
||||
using Recyclarr.TrashGuide.ReleaseProfile;
|
||||
using Spectre.Console.Testing;
|
||||
|
||||
namespace Recyclarr.Cli.Tests.Pipelines.ReleaseProfile;
|
||||
|
||||
[TestFixture]
|
||||
public class ReleaseProfileDataListerTest
|
||||
{
|
||||
[Test, AutoMockData]
|
||||
public void Release_profiles_appear_in_console_output(
|
||||
[Frozen(Matching.ImplementedInterfaces)] TestConsole console,
|
||||
[Frozen] IReleaseProfileGuideService guide,
|
||||
ReleaseProfileDataLister sut)
|
||||
{
|
||||
var testData = new[]
|
||||
{
|
||||
new ReleaseProfileData {Name = "First", TrashId = "123"},
|
||||
new ReleaseProfileData {Name = "Second", TrashId = "456"}
|
||||
};
|
||||
|
||||
guide.GetReleaseProfileData().Returns(testData);
|
||||
|
||||
sut.ListReleaseProfiles();
|
||||
|
||||
console.Output.Should().ContainAll(
|
||||
testData.SelectMany(x => new[] {x.Name, x.TrashId}));
|
||||
}
|
||||
|
||||
[Test, AutoMockData]
|
||||
public void Terms_appear_in_console_output(
|
||||
[Frozen] IReleaseProfileGuideService guide,
|
||||
[Frozen(Matching.ImplementedInterfaces)] TestConsole console,
|
||||
ReleaseProfileDataLister sut)
|
||||
{
|
||||
var requiredData = new[]
|
||||
{
|
||||
new TermData {Name = "First", TrashId = "111", Term = "term1"},
|
||||
new TermData {Name = "Second", TrashId = "222", Term = "term2"}
|
||||
};
|
||||
|
||||
var ignoredData = new[]
|
||||
{
|
||||
new TermData {Name = "Third", TrashId = "333", Term = "term3"},
|
||||
new TermData {Name = "Fourth", TrashId = "444", Term = "term4"}
|
||||
};
|
||||
|
||||
var preferredData = new[]
|
||||
{
|
||||
new TermData {Name = "Fifth", TrashId = "555", Term = "term5"},
|
||||
new TermData {Name = "Sixth", TrashId = "666", Term = "term6"}
|
||||
};
|
||||
|
||||
guide.GetReleaseProfileData().Returns(new[]
|
||||
{
|
||||
new ReleaseProfileData
|
||||
{
|
||||
Name = "Release Profile",
|
||||
TrashId = "098",
|
||||
Required = requiredData,
|
||||
Ignored = ignoredData,
|
||||
Preferred = new PreferredTermData[]
|
||||
{
|
||||
new() {Score = 100, Terms = preferredData}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
sut.ListTerms("098");
|
||||
|
||||
var expectedOutput = new[]
|
||||
{
|
||||
requiredData.SelectMany(x => new[] {x.Name, x.TrashId}),
|
||||
ignoredData.SelectMany(x => new[] {x.Name, x.TrashId}),
|
||||
preferredData.SelectMany(x => new[] {x.Name, x.TrashId})
|
||||
};
|
||||
|
||||
console.Output.Should().ContainAll(expectedOutput.SelectMany(x => x));
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
using Recyclarr.Cli.Pipelines.Tags;
|
||||
using Recyclarr.Cli.Pipelines.Tags.PipelinePhases;
|
||||
using Recyclarr.ServarrApi.Tag;
|
||||
|
||||
namespace Recyclarr.Cli.Tests.Pipelines.Tags.PipelinePhases;
|
||||
|
||||
[TestFixture]
|
||||
public class TagApiFetchPhaseTest
|
||||
{
|
||||
[Test, AutoMockData]
|
||||
public async Task Cache_is_updated(
|
||||
[Frozen] ISonarrTagApiService api,
|
||||
[Frozen] ServiceTagCache cache,
|
||||
TagPipelineContext context,
|
||||
TagApiFetchPhase sut)
|
||||
{
|
||||
var expectedData = new[]
|
||||
{
|
||||
new SonarrTag {Id = 3},
|
||||
new SonarrTag {Id = 4},
|
||||
new SonarrTag {Id = 5}
|
||||
};
|
||||
|
||||
api.GetTags(default!).ReturnsForAnyArgs(expectedData);
|
||||
|
||||
await sut.Execute(context, default!);
|
||||
cache.Tags.Should().BeEquivalentTo(expectedData);
|
||||
}
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
using Recyclarr.Cli.Pipelines.Tags;
|
||||
using Recyclarr.Cli.Pipelines.Tags.PipelinePhases;
|
||||
using Recyclarr.Config.Models;
|
||||
using Recyclarr.ServarrApi.Tag;
|
||||
|
||||
namespace Recyclarr.Cli.Tests.Pipelines.Tags.PipelinePhases;
|
||||
|
||||
[TestFixture]
|
||||
public class TagApiPersistencePhaseTest
|
||||
{
|
||||
[Test, AutoMockData]
|
||||
public async Task Persisted_tags_are_added_to_cache(
|
||||
[Frozen] ISonarrTagApiService api,
|
||||
[Frozen] ServiceTagCache cache,
|
||||
TagApiPersistencePhase sut)
|
||||
{
|
||||
cache.AddTags(new[]
|
||||
{
|
||||
new SonarrTag {Id = 1},
|
||||
new SonarrTag {Id = 2}
|
||||
});
|
||||
|
||||
var config = Substitute.For<IServiceConfiguration>();
|
||||
var context = new TagPipelineContext
|
||||
{
|
||||
TransactionOutput = new[] {"three", "four"}
|
||||
};
|
||||
|
||||
api.CreateTag(config, "three").Returns(new SonarrTag {Id = 3});
|
||||
api.CreateTag(config, "four").Returns(new SonarrTag {Id = 4});
|
||||
|
||||
await sut.Execute(context, config);
|
||||
|
||||
cache.Tags.Should().BeEquivalentTo(new[]
|
||||
{
|
||||
new SonarrTag {Id = 1},
|
||||
new SonarrTag {Id = 2},
|
||||
new SonarrTag {Id = 3},
|
||||
new SonarrTag {Id = 4}
|
||||
});
|
||||
}
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
using Recyclarr.Cli.Pipelines.Tags;
|
||||
using Recyclarr.Cli.Pipelines.Tags.PipelinePhases;
|
||||
using Recyclarr.Config.Models;
|
||||
using Recyclarr.Tests.TestLibrary;
|
||||
|
||||
namespace Recyclarr.Cli.Tests.Pipelines.Tags.PipelinePhases;
|
||||
|
||||
[TestFixture]
|
||||
public class TagConfigPhaseTest
|
||||
{
|
||||
[Test, AutoMockData]
|
||||
public async Task Output_empty_when_config_has_no_tags(TagConfigPhase sut)
|
||||
{
|
||||
var context = new TagPipelineContext();
|
||||
var config = NewConfig.Sonarr() with
|
||||
{
|
||||
ReleaseProfiles = Array.Empty<ReleaseProfileConfig>()
|
||||
};
|
||||
|
||||
await sut.Execute(context, config);
|
||||
context.ConfigOutput.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Test, AutoMockData]
|
||||
public void Output_not_empty_when_config_has_tags(TagConfigPhase sut)
|
||||
{
|
||||
var config = NewConfig.Sonarr() with
|
||||
{
|
||||
ReleaseProfiles = new[]
|
||||
{
|
||||
new ReleaseProfileConfig
|
||||
{
|
||||
Tags = new[] {"one", "two", "three"}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var context = new TagPipelineContext();
|
||||
sut.Execute(context, config);
|
||||
context.ConfigOutput.Should().BeEquivalentTo(config.ReleaseProfiles[0].Tags);
|
||||
}
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
using Recyclarr.Cli.Pipelines.Tags;
|
||||
using Recyclarr.Cli.Pipelines.Tags.PipelinePhases;
|
||||
using Recyclarr.Config.Models;
|
||||
using Recyclarr.ServarrApi.Tag;
|
||||
|
||||
namespace Recyclarr.Cli.Tests.Pipelines.Tags.PipelinePhases;
|
||||
|
||||
[TestFixture]
|
||||
public class TagTransactionPhaseTest
|
||||
{
|
||||
[Test, AutoMockData]
|
||||
public void Return_tags_in_config_that_do_not_exist_in_service(TagTransactionPhase sut)
|
||||
{
|
||||
var context = new TagPipelineContext
|
||||
{
|
||||
ConfigOutput = new[] {"one", "two", "three"},
|
||||
ApiFetchOutput = new[]
|
||||
{
|
||||
new SonarrTag {Label = "three"},
|
||||
new SonarrTag {Label = "four"}
|
||||
}
|
||||
};
|
||||
|
||||
sut.Execute(context, Substitute.For<IServiceConfiguration>());
|
||||
|
||||
context.TransactionOutput.Should().BeEquivalentTo("one", "two");
|
||||
}
|
||||
|
||||
[Test, AutoMockData]
|
||||
public void Return_all_tags_if_none_exist(TagTransactionPhase sut)
|
||||
{
|
||||
var context = new TagPipelineContext
|
||||
{
|
||||
ConfigOutput = new[] {"one", "two", "three"},
|
||||
ApiFetchOutput = Array.Empty<SonarrTag>()
|
||||
};
|
||||
|
||||
sut.Execute(context, Substitute.For<IServiceConfiguration>());
|
||||
|
||||
context.TransactionOutput.Should().BeEquivalentTo("one", "two", "three");
|
||||
}
|
||||
|
||||
[Test, AutoMockData]
|
||||
public void No_tags_returned_if_all_exist(TagTransactionPhase sut)
|
||||
{
|
||||
var context = new TagPipelineContext
|
||||
{
|
||||
ConfigOutput = Array.Empty<string>(),
|
||||
ApiFetchOutput = new[]
|
||||
{
|
||||
new SonarrTag {Label = "three"},
|
||||
new SonarrTag {Label = "four"}
|
||||
}
|
||||
};
|
||||
|
||||
sut.Execute(context, Substitute.For<IServiceConfiguration>());
|
||||
|
||||
context.TransactionOutput.Should().BeEmpty();
|
||||
}
|
||||
}
|
@ -1,130 +0,0 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Recyclarr.Config.Parsing;
|
||||
using Recyclarr.Config.Parsing.PostProcessing.ConfigMerging;
|
||||
|
||||
namespace Recyclarr.Tests.Config.Parsing.PostProcessing.ConfigMerging;
|
||||
|
||||
[TestFixture]
|
||||
public class MergeReleaseProfilesTest
|
||||
{
|
||||
[Test]
|
||||
public void Empty_right_to_non_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml
|
||||
{
|
||||
ReleaseProfiles = new[]
|
||||
{
|
||||
new ReleaseProfileConfigYaml
|
||||
{
|
||||
TrashIds = new[] {"id1"},
|
||||
Filter = new ReleaseProfileFilterConfigYaml
|
||||
{
|
||||
Exclude = new[] {"exclude"},
|
||||
Include = new[] {"include"}
|
||||
},
|
||||
Tags = new[] {"tag1", "tag2"},
|
||||
StrictNegativeScores = true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
{
|
||||
ReleaseProfiles = new[]
|
||||
{
|
||||
new ReleaseProfileConfigYaml
|
||||
{
|
||||
TrashIds = new[] {"id1"},
|
||||
Filter = new ReleaseProfileFilterConfigYaml
|
||||
{
|
||||
Exclude = new[] {"exclude"},
|
||||
Include = new[] {"include"}
|
||||
},
|
||||
Tags = new[] {"tag1", "tag2"},
|
||||
StrictNegativeScores = true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
{
|
||||
ReleaseProfiles = new[]
|
||||
{
|
||||
new ReleaseProfileConfigYaml
|
||||
{
|
||||
TrashIds = new[] {"id1"},
|
||||
Filter = new ReleaseProfileFilterConfigYaml
|
||||
{
|
||||
Exclude = new[] {"exclude1"},
|
||||
Include = new[] {"include1"}
|
||||
},
|
||||
Tags = new[] {"tag1", "tag2"},
|
||||
StrictNegativeScores = true
|
||||
},
|
||||
new ReleaseProfileConfigYaml
|
||||
{
|
||||
TrashIds = new[] {"id2", "id3"},
|
||||
Filter = new ReleaseProfileFilterConfigYaml
|
||||
{
|
||||
Exclude = new[] {"exclude2"},
|
||||
Include = new[] {"include2"}
|
||||
},
|
||||
Tags = new[] {"tag3"},
|
||||
StrictNegativeScores = true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var rightConfig = new SonarrConfigYaml
|
||||
{
|
||||
ReleaseProfiles = new[]
|
||||
{
|
||||
new ReleaseProfileConfigYaml
|
||||
{
|
||||
TrashIds = new[] {"id4"},
|
||||
Filter = new ReleaseProfileFilterConfigYaml
|
||||
{
|
||||
Exclude = new[] {"exclude3"},
|
||||
Include = new[] {"include3"}
|
||||
},
|
||||
Tags = new[] {"tag4", "tag5"},
|
||||
StrictNegativeScores = false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(new SonarrConfigYaml
|
||||
{
|
||||
ReleaseProfiles = leftConfig.ReleaseProfiles.Concat(rightConfig.ReleaseProfiles).ToList()
|
||||
});
|
||||
}
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
using FluentValidation.TestHelper;
|
||||
using Recyclarr.Config.Models;
|
||||
using Recyclarr.Config.Parsing;
|
||||
|
||||
namespace Recyclarr.Tests.Config.Parsing;
|
||||
|
||||
[TestFixture]
|
||||
public class SonarrConfigYamlValidatorTest
|
||||
{
|
||||
[Test]
|
||||
public void Validation_failure_when_rps_and_cfs_used_together()
|
||||
{
|
||||
var config = new SonarrConfigYaml
|
||||
{
|
||||
ReleaseProfiles = new[] {new ReleaseProfileConfigYaml()},
|
||||
CustomFormats = new[] {new CustomFormatConfigYaml()}
|
||||
};
|
||||
|
||||
var validator = new SonarrConfigYamlValidator();
|
||||
var result = validator.TestValidate(config);
|
||||
|
||||
result.ShouldHaveValidationErrorFor(x => x)
|
||||
.WithErrorMessage("`custom_formats` and `release_profiles` may not be used together");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Sonarr_release_profile_failures()
|
||||
{
|
||||
var config = new ReleaseProfileConfigYaml
|
||||
{
|
||||
TrashIds = Array.Empty<string>(),
|
||||
Filter = new ReleaseProfileFilterConfigYaml
|
||||
{
|
||||
Include = new[] {"include"},
|
||||
Exclude = new[] {"exclude"}
|
||||
}
|
||||
};
|
||||
|
||||
var validator = new ReleaseProfileConfigYamlValidator();
|
||||
var result = validator.TestValidate(config);
|
||||
|
||||
result.Errors.Should().HaveCount(2);
|
||||
|
||||
// Release profile trash IDs cannot be empty
|
||||
result.ShouldHaveValidationErrorFor(x => x.TrashIds);
|
||||
|
||||
// Cannot use include + exclude filters together
|
||||
result.ShouldHaveValidationErrorFor(nameof(ReleaseProfileConfig.Filter));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Filter_include_can_not_be_empty()
|
||||
{
|
||||
var config = new ReleaseProfileFilterConfigYaml
|
||||
{
|
||||
Include = Array.Empty<string>(),
|
||||
Exclude = new[] {"exclude"}
|
||||
};
|
||||
|
||||
var validator = new ReleaseProfileFilterConfigYamlValidator();
|
||||
var result = validator.TestValidate(config);
|
||||
|
||||
result.Errors.Should().HaveCount(1);
|
||||
|
||||
result.ShouldHaveValidationErrorFor(x => x.Include);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Filter_exclude_can_not_be_empty()
|
||||
{
|
||||
var config = new ReleaseProfileFilterConfigYaml
|
||||
{
|
||||
Exclude = Array.Empty<string>(),
|
||||
Include = new[] {"exclude"}
|
||||
};
|
||||
|
||||
var validator = new ReleaseProfileFilterConfigYamlValidator();
|
||||
var result = validator.TestValidate(config);
|
||||
|
||||
result.Errors.Should().HaveCount(1);
|
||||
|
||||
result.ShouldHaveValidationErrorFor(x => x.Exclude);
|
||||
}
|
||||
}
|
@ -1,74 +0,0 @@
|
||||
using Recyclarr.TrashGuide.ReleaseProfile;
|
||||
|
||||
namespace Recyclarr.Tests.TrashGuide.ReleaseProfile;
|
||||
|
||||
[TestFixture]
|
||||
public class ReleaseProfileDataValidationFiltererTest
|
||||
{
|
||||
[Test, AutoMockData]
|
||||
public void Valid_data_is_not_filtered_out(ReleaseProfileDataValidationFilterer sut)
|
||||
{
|
||||
var data = new[]
|
||||
{
|
||||
new ReleaseProfileData
|
||||
{
|
||||
TrashId = "trash_id",
|
||||
Name = "name",
|
||||
Required = new[] {new TermData {Term = "term1"}},
|
||||
Ignored = new[] {new TermData {Term = "term2"}},
|
||||
Preferred = new[] {new PreferredTermData {Terms = new[] {new TermData {Term = "term3"}}}}
|
||||
}
|
||||
};
|
||||
|
||||
var result = sut.FilterProfiles(data);
|
||||
|
||||
result.Should().BeEquivalentTo(data);
|
||||
}
|
||||
|
||||
[Test, AutoMockData]
|
||||
public void Invalid_terms_are_filtered_out(ReleaseProfileDataValidationFilterer sut)
|
||||
{
|
||||
var data = new[]
|
||||
{
|
||||
new ReleaseProfileData
|
||||
{
|
||||
TrashId = "trash_id",
|
||||
Name = "name",
|
||||
Required = new[] {new TermData {Term = ""}},
|
||||
Ignored = new[] {new TermData {Term = "term2"}},
|
||||
Preferred = new[] {new PreferredTermData {Terms = new[] {new TermData {Term = "term3"}}}}
|
||||
}
|
||||
};
|
||||
|
||||
var result = sut.FilterProfiles(data);
|
||||
|
||||
result.Should().ContainSingle().Which.Should().BeEquivalentTo(new ReleaseProfileData
|
||||
{
|
||||
TrashId = "trash_id",
|
||||
Name = "name",
|
||||
Required = Array.Empty<TermData>(),
|
||||
Ignored = new[] {new TermData {Term = "term2"}},
|
||||
Preferred = new[] {new PreferredTermData {Terms = new[] {new TermData {Term = "term3"}}}}
|
||||
});
|
||||
}
|
||||
|
||||
[Test, AutoMockData]
|
||||
public void Whole_release_profile_filtered_out_if_all_terms_invalid(ReleaseProfileDataValidationFilterer sut)
|
||||
{
|
||||
var data = new[]
|
||||
{
|
||||
new ReleaseProfileData
|
||||
{
|
||||
TrashId = "trash_id",
|
||||
Name = "name",
|
||||
Required = new[] {new TermData {Term = ""}},
|
||||
Ignored = new[] {new TermData {Term = ""}},
|
||||
Preferred = new[] {new PreferredTermData {Terms = new[] {new TermData {Term = ""}}}}
|
||||
}
|
||||
};
|
||||
|
||||
var result = sut.FilterProfiles(data);
|
||||
|
||||
result.Should().BeEmpty();
|
||||
}
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
using FluentValidation.TestHelper;
|
||||
using Recyclarr.TrashGuide.ReleaseProfile;
|
||||
|
||||
namespace Recyclarr.Tests.TrashGuide.ReleaseProfile;
|
||||
|
||||
[TestFixture]
|
||||
public class ReleaseProfileDataValidatorTest
|
||||
{
|
||||
[Test]
|
||||
public void Empty_term_collections_not_allowed()
|
||||
{
|
||||
var validator = new ReleaseProfileDataValidator();
|
||||
var data = new ReleaseProfileData();
|
||||
|
||||
validator.Validate(data).IsValid.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Allow_single_preferred_term()
|
||||
{
|
||||
var validator = new ReleaseProfileDataValidator();
|
||||
var data = new ReleaseProfileData
|
||||
{
|
||||
TrashId = "trash_id",
|
||||
Name = "name",
|
||||
Required = Array.Empty<TermData>(),
|
||||
Ignored = Array.Empty<TermData>(),
|
||||
Preferred = new[] {new PreferredTermData {Terms = new[] {new TermData()}}}
|
||||
};
|
||||
|
||||
var result = validator.TestValidate(data);
|
||||
|
||||
result.ShouldNotHaveAnyValidationErrors();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Allow_single_required_term()
|
||||
{
|
||||
var validator = new ReleaseProfileDataValidator();
|
||||
var data = new ReleaseProfileData
|
||||
{
|
||||
TrashId = "trash_id",
|
||||
Name = "name",
|
||||
Required = new[] {new TermData {Term = "term"}},
|
||||
Ignored = Array.Empty<TermData>(),
|
||||
Preferred = Array.Empty<PreferredTermData>()
|
||||
};
|
||||
|
||||
var result = validator.TestValidate(data);
|
||||
|
||||
result.ShouldNotHaveAnyValidationErrors();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Allow_single_ignored_term()
|
||||
{
|
||||
var validator = new ReleaseProfileDataValidator();
|
||||
var data = new ReleaseProfileData
|
||||
{
|
||||
TrashId = "trash_id",
|
||||
Name = "name",
|
||||
Required = Array.Empty<TermData>(),
|
||||
Ignored = new[] {new TermData {Term = "term"}},
|
||||
Preferred = Array.Empty<PreferredTermData>()
|
||||
};
|
||||
|
||||
var result = validator.TestValidate(data);
|
||||
|
||||
result.ShouldNotHaveAnyValidationErrors();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Term_data_validate_empty()
|
||||
{
|
||||
var validator = new TermDataValidator();
|
||||
var data = new TermData();
|
||||
|
||||
var result = validator.TestValidate(data);
|
||||
|
||||
result.ShouldHaveValidationErrorFor(x => x.Term);
|
||||
result.ShouldNotHaveValidationErrorFor(x => x.Name);
|
||||
result.ShouldNotHaveValidationErrorFor(x => x.TrashId);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Preferred_term_data_validate_empty()
|
||||
{
|
||||
var validator = new PreferredTermDataValidator();
|
||||
var data = new PreferredTermData();
|
||||
|
||||
var result = validator.TestValidate(data);
|
||||
|
||||
result.ShouldHaveValidationErrorFor(x => x.Terms);
|
||||
}
|
||||
}
|
@ -1,85 +0,0 @@
|
||||
using System.IO.Abstractions;
|
||||
using Recyclarr.Json;
|
||||
using Recyclarr.Repo;
|
||||
using Recyclarr.TestLibrary;
|
||||
using Recyclarr.TrashGuide.ReleaseProfile;
|
||||
|
||||
namespace Recyclarr.Tests.TrashGuide.ReleaseProfile;
|
||||
|
||||
[TestFixture]
|
||||
public class ReleaseProfileGuideServiceTest
|
||||
{
|
||||
[Test, AutoMockData]
|
||||
public void Get_release_profile_json_works(
|
||||
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
|
||||
[Frozen] IRepoMetadataBuilder metadataBuilder,
|
||||
ReleaseProfileGuideService sut)
|
||||
{
|
||||
static ReleaseProfileData MakeMockObject(string term)
|
||||
{
|
||||
return new ReleaseProfileData
|
||||
{
|
||||
Name = "name",
|
||||
TrashId = "123",
|
||||
Required = new TermData[]
|
||||
{
|
||||
new() {Term = term}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
var mockData1 = MakeMockObject("first");
|
||||
var mockData2 = MakeMockObject("second");
|
||||
var baseDir = fs.CurrentDirectory().SubDirectory("files");
|
||||
baseDir.Create();
|
||||
|
||||
fs.AddFile(baseDir.File("first.json").FullName,
|
||||
MockData.FromJson(mockData1, GlobalJsonSerializerSettings.Services));
|
||||
|
||||
fs.AddFile(baseDir.File("second.json").FullName,
|
||||
MockData.FromJson(mockData2, GlobalJsonSerializerSettings.Services));
|
||||
|
||||
metadataBuilder.ToDirectoryInfoList(default!).ReturnsForAnyArgs(new[] {baseDir});
|
||||
|
||||
var results = sut.GetReleaseProfileData();
|
||||
|
||||
results.Should().BeEquivalentTo(new[]
|
||||
{
|
||||
mockData1,
|
||||
mockData2
|
||||
});
|
||||
}
|
||||
|
||||
[Test, AutoMockData]
|
||||
public void Json_exceptions_do_not_interrupt_parsing_other_files(
|
||||
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
|
||||
[Frozen] IRepoMetadataBuilder metadataBuilder,
|
||||
ReleaseProfileGuideService sut)
|
||||
{
|
||||
var rootPath = fs.CurrentDirectory().SubDirectory("files");
|
||||
rootPath.Create();
|
||||
|
||||
var badData = "# comment";
|
||||
var goodData = new ReleaseProfileData
|
||||
{
|
||||
Name = "name",
|
||||
TrashId = "123",
|
||||
Required = new TermData[]
|
||||
{
|
||||
new() {Term = "abc"}
|
||||
}
|
||||
};
|
||||
|
||||
fs.AddFile(rootPath.File("0_bad_data.json").FullName,
|
||||
MockData.FromString(badData));
|
||||
|
||||
fs.AddFile(rootPath.File("1_good_data.json").FullName,
|
||||
MockData.FromJson(goodData, GlobalJsonSerializerSettings.Services));
|
||||
|
||||
metadataBuilder.ToDirectoryInfoList(default!).ReturnsForAnyArgs(new[] {rootPath});
|
||||
|
||||
var results = sut.GetReleaseProfileData();
|
||||
|
||||
results.Should().BeEquivalentTo(new[] {goodData});
|
||||
}
|
||||
}
|
Loading…
Reference in new issue