New: Custom colon replacement option

Closes #6898
This commit is contained in:
Mark McDowall 2024-07-09 22:02:04 -07:00 committed by GitHub
parent 0c883f7886
commit 293a1bc618
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 100 additions and 7 deletions

View File

@ -138,7 +138,8 @@ class Naming extends Component {
{ key: 1, value: translate('ReplaceWithDash') }, { key: 1, value: translate('ReplaceWithDash') },
{ key: 2, value: translate('ReplaceWithSpaceDash') }, { key: 2, value: translate('ReplaceWithSpaceDash') },
{ key: 3, value: translate('ReplaceWithSpaceDashSpace') }, { key: 3, value: translate('ReplaceWithSpaceDashSpace') },
{ key: 4, value: translate('SmartReplace'), hint: translate('SmartReplaceHint') } { key: 4, value: translate('SmartReplace'), hint: translate('SmartReplaceHint') },
{ key: 5, value: translate('Custom'), hint: translate('CustomColonReplacementFormatHint') }
]; ];
const standardEpisodeFormatHelpTexts = []; const standardEpisodeFormatHelpTexts = [];
@ -262,6 +263,22 @@ class Naming extends Component {
null null
} }
{
replaceIllegalCharacters && settings.colonReplacementFormat.value === 5 ?
<FormGroup>
<FormLabel>{translate('ColonReplacement')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="customColonReplacementFormat"
helpText={translate('CustomColonReplacementFormatHelpText')}
onChange={onInputChange}
{...settings.customColonReplacementFormat}
/>
</FormGroup> :
null
}
{ {
renameEpisodes && renameEpisodes &&
<div> <div>

View File

@ -90,5 +90,18 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
Subject.BuildFileName(new List<Episode> { _episode1 }, _series, _episodeFile) Subject.BuildFileName(new List<Episode> { _episode1 }, _series, _episodeFile)
.Should().Be(expected); .Should().Be(expected);
} }
[TestCase("Series: Title", ColonReplacementFormat.Custom, "\ua789", "Series\ua789 Title")]
[TestCase("Series: Title", ColonReplacementFormat.Custom, "", "Series Title")]
public void should_replace_colon_with_custom_format(string seriesName, ColonReplacementFormat replacementFormat, string customFormat, string expected)
{
_series.Title = seriesName;
_namingConfig.StandardEpisodeFormat = "{Series Title}";
_namingConfig.ColonReplacementFormat = replacementFormat;
_namingConfig.CustomColonReplacementFormat = customFormat;
Subject.BuildFileName(new List<Episode> { _episode1 }, _series, _episodeFile)
.Should().Be(expected);
}
} }
} }

View File

@ -0,0 +1,14 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(211)]
public class add_custom_colon_replacement_to_naming_config : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Alter.Table("NamingConfig").AddColumn("CustomColonReplacementFormat").AsString().WithDefaultValue("");
}
}
}

View File

@ -270,6 +270,9 @@
"CreateGroup": "Create Group", "CreateGroup": "Create Group",
"CurrentlyInstalled": "Currently Installed", "CurrentlyInstalled": "Currently Installed",
"Custom": "Custom", "Custom": "Custom",
"CustomColonReplacement": "Custom Colon Replacement",
"CustomColonReplacementFormatHelpText": "Characters to be used as a replacement for colons",
"CustomColonReplacementFormatHint": "Valid file system character such as Colon (Letter)",
"CustomFilter": "Custom Filter", "CustomFilter": "Custom Filter",
"CustomFilters": "Custom Filters", "CustomFilters": "Custom Filters",
"CustomFormat": "Custom Format", "CustomFormat": "Custom Format",

View File

@ -6,7 +6,6 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Diacritical; using Diacritical;
using DryIoc.ImTools;
using NLog; using NLog;
using NzbDrone.Common.Cache; using NzbDrone.Common.Cache;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
@ -112,6 +111,9 @@ namespace NzbDrone.Core.Organizer
{ "wel", "cym" } { "wel", "cym" }
}.ToImmutableDictionary(); }.ToImmutableDictionary();
public static readonly ImmutableArray<string> BadCharacters = ImmutableArray.Create("\\", "/", "<", ">", "?", "*", "|", "\"");
public static readonly ImmutableArray<string> GoodCharacters = ImmutableArray.Create("+", "+", "", "", "!", "-", "", "");
public FileNameBuilder(INamingConfigService namingConfigService, public FileNameBuilder(INamingConfigService namingConfigService,
IQualityDefinitionService qualityDefinitionService, IQualityDefinitionService qualityDefinitionService,
ICacheManager cacheManager, ICacheManager cacheManager,
@ -1156,8 +1158,6 @@ namespace NzbDrone.Core.Organizer
private static string CleanFileName(string name, NamingConfig namingConfig) private static string CleanFileName(string name, NamingConfig namingConfig)
{ {
var result = name; var result = name;
string[] badCharacters = { "\\", "/", "<", ">", "?", "*", "|", "\"" };
string[] goodCharacters = { "+", "+", "", "", "!", "-", "", "" };
if (namingConfig.ReplaceIllegalCharacters) if (namingConfig.ReplaceIllegalCharacters)
{ {
@ -1182,6 +1182,9 @@ namespace NzbDrone.Core.Organizer
case ColonReplacementFormat.SpaceDashSpace: case ColonReplacementFormat.SpaceDashSpace:
replacement = " - "; replacement = " - ";
break; break;
case ColonReplacementFormat.Custom:
replacement = namingConfig.CustomColonReplacementFormat;
break;
} }
result = result.Replace(":", replacement); result = result.Replace(":", replacement);
@ -1192,9 +1195,9 @@ namespace NzbDrone.Core.Organizer
result = result.Replace(":", string.Empty); result = result.Replace(":", string.Empty);
} }
for (var i = 0; i < badCharacters.Length; i++) for (var i = 0; i < BadCharacters.Length; i++)
{ {
result = result.Replace(badCharacters[i], namingConfig.ReplaceIllegalCharacters ? goodCharacters[i] : string.Empty); result = result.Replace(BadCharacters[i], namingConfig.ReplaceIllegalCharacters ? GoodCharacters[i] : string.Empty);
} }
return result.TrimStart(' ', '.').TrimEnd(' '); return result.TrimStart(' ', '.').TrimEnd(' ');
@ -1268,6 +1271,7 @@ namespace NzbDrone.Core.Organizer
Dash = 1, Dash = 1,
SpaceDash = 2, SpaceDash = 2,
SpaceDashSpace = 3, SpaceDashSpace = 3,
Smart = 4 Smart = 4,
Custom = 5
} }
} }

View File

@ -1,3 +1,4 @@
using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@ -61,6 +62,13 @@ namespace NzbDrone.Core.Organizer
return ruleBuilder.SetValidator(new IllegalCharactersValidator()); return ruleBuilder.SetValidator(new IllegalCharactersValidator());
} }
public static IRuleBuilderOptions<T, string> ValidCustomColonReplacement<T>(this IRuleBuilder<T, string> ruleBuilder)
{
ruleBuilder.SetValidator(new IllegalColonCharactersValidator());
return ruleBuilder.SetValidator(new IllegalCharactersValidator());
}
} }
public class ValidStandardEpisodeFormatValidator : PropertyValidator public class ValidStandardEpisodeFormatValidator : PropertyValidator
@ -132,6 +140,34 @@ namespace NzbDrone.Core.Organizer
} }
var invalidCharacters = InvalidPathChars.Where(i => value!.IndexOf(i) >= 0).ToList(); var invalidCharacters = InvalidPathChars.Where(i => value!.IndexOf(i) >= 0).ToList();
if (invalidCharacters.Any())
{
context.MessageFormatter.AppendArgument("InvalidCharacters", string.Join("", invalidCharacters));
return false;
}
return true;
}
}
public class IllegalColonCharactersValidator : PropertyValidator
{
private static readonly string[] InvalidPathChars = FileNameBuilder.BadCharacters.Concat(new[] { ":" }).ToArray();
protected override string GetDefaultMessageTemplate() => "Contains illegal characters: {InvalidCharacters}";
protected override bool IsValid(PropertyValidatorContext context)
{
var value = context.PropertyValue as string;
if (value.IsNullOrWhiteSpace())
{
return true;
}
var invalidCharacters = InvalidPathChars.Where(i => value!.IndexOf(i, StringComparison.Ordinal) >= 0).ToList();
if (invalidCharacters.Any()) if (invalidCharacters.Any())
{ {
context.MessageFormatter.AppendArgument("InvalidCharacters", string.Join("", invalidCharacters)); context.MessageFormatter.AppendArgument("InvalidCharacters", string.Join("", invalidCharacters));

View File

@ -9,6 +9,7 @@ namespace NzbDrone.Core.Organizer
RenameEpisodes = false, RenameEpisodes = false,
ReplaceIllegalCharacters = true, ReplaceIllegalCharacters = true,
ColonReplacementFormat = ColonReplacementFormat.Smart, ColonReplacementFormat = ColonReplacementFormat.Smart,
CustomColonReplacementFormat = string.Empty,
MultiEpisodeStyle = MultiEpisodeStyle.PrefixedRange, MultiEpisodeStyle = MultiEpisodeStyle.PrefixedRange,
StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Quality Full}", StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Quality Full}",
DailyEpisodeFormat = "{Series Title} - {Air-Date} - {Episode Title} {Quality Full}", DailyEpisodeFormat = "{Series Title} - {Air-Date} - {Episode Title} {Quality Full}",
@ -21,6 +22,7 @@ namespace NzbDrone.Core.Organizer
public bool RenameEpisodes { get; set; } public bool RenameEpisodes { get; set; }
public bool ReplaceIllegalCharacters { get; set; } public bool ReplaceIllegalCharacters { get; set; }
public ColonReplacementFormat ColonReplacementFormat { get; set; } public ColonReplacementFormat ColonReplacementFormat { get; set; }
public string CustomColonReplacementFormat { get; set; }
public MultiEpisodeStyle MultiEpisodeStyle { get; set; } public MultiEpisodeStyle MultiEpisodeStyle { get; set; }
public string StandardEpisodeFormat { get; set; } public string StandardEpisodeFormat { get; set; }
public string DailyEpisodeFormat { get; set; } public string DailyEpisodeFormat { get; set; }

View File

@ -36,6 +36,7 @@ namespace Sonarr.Api.V3.Config
SharedValidator.RuleFor(c => c.SeriesFolderFormat).ValidSeriesFolderFormat(); SharedValidator.RuleFor(c => c.SeriesFolderFormat).ValidSeriesFolderFormat();
SharedValidator.RuleFor(c => c.SeasonFolderFormat).ValidSeasonFolderFormat(); SharedValidator.RuleFor(c => c.SeasonFolderFormat).ValidSeasonFolderFormat();
SharedValidator.RuleFor(c => c.SpecialsFolderFormat).ValidSpecialsFolderFormat(); SharedValidator.RuleFor(c => c.SpecialsFolderFormat).ValidSpecialsFolderFormat();
SharedValidator.RuleFor(c => c.CustomColonReplacementFormat).ValidCustomColonReplacement().When(c => c.ColonReplacementFormat == (int)ColonReplacementFormat.Custom);
} }
protected override NamingConfigResource GetResourceById(int id) protected override NamingConfigResource GetResourceById(int id)

View File

@ -7,6 +7,7 @@ namespace Sonarr.Api.V3.Config
public bool RenameEpisodes { get; set; } public bool RenameEpisodes { get; set; }
public bool ReplaceIllegalCharacters { get; set; } public bool ReplaceIllegalCharacters { get; set; }
public int ColonReplacementFormat { get; set; } public int ColonReplacementFormat { get; set; }
public string CustomColonReplacementFormat { get; set; }
public int MultiEpisodeStyle { get; set; } public int MultiEpisodeStyle { get; set; }
public string StandardEpisodeFormat { get; set; } public string StandardEpisodeFormat { get; set; }
public string DailyEpisodeFormat { get; set; } public string DailyEpisodeFormat { get; set; }

View File

@ -25,6 +25,7 @@ namespace Sonarr.Api.V3.Config
RenameEpisodes = model.RenameEpisodes, RenameEpisodes = model.RenameEpisodes,
ReplaceIllegalCharacters = model.ReplaceIllegalCharacters, ReplaceIllegalCharacters = model.ReplaceIllegalCharacters,
ColonReplacementFormat = (int)model.ColonReplacementFormat, ColonReplacementFormat = (int)model.ColonReplacementFormat,
CustomColonReplacementFormat = model.CustomColonReplacementFormat,
MultiEpisodeStyle = (int)model.MultiEpisodeStyle, MultiEpisodeStyle = (int)model.MultiEpisodeStyle,
StandardEpisodeFormat = model.StandardEpisodeFormat, StandardEpisodeFormat = model.StandardEpisodeFormat,
DailyEpisodeFormat = model.DailyEpisodeFormat, DailyEpisodeFormat = model.DailyEpisodeFormat,
@ -45,6 +46,7 @@ namespace Sonarr.Api.V3.Config
ReplaceIllegalCharacters = resource.ReplaceIllegalCharacters, ReplaceIllegalCharacters = resource.ReplaceIllegalCharacters,
MultiEpisodeStyle = (MultiEpisodeStyle)resource.MultiEpisodeStyle, MultiEpisodeStyle = (MultiEpisodeStyle)resource.MultiEpisodeStyle,
ColonReplacementFormat = (ColonReplacementFormat)resource.ColonReplacementFormat, ColonReplacementFormat = (ColonReplacementFormat)resource.ColonReplacementFormat,
CustomColonReplacementFormat = resource.CustomColonReplacementFormat,
StandardEpisodeFormat = resource.StandardEpisodeFormat, StandardEpisodeFormat = resource.StandardEpisodeFormat,
DailyEpisodeFormat = resource.DailyEpisodeFormat, DailyEpisodeFormat = resource.DailyEpisodeFormat,
AnimeEpisodeFormat = resource.AnimeEpisodeFormat, AnimeEpisodeFormat = resource.AnimeEpisodeFormat,