diff --git a/frontend/src/Settings/MediaManagement/Naming/Naming.js b/frontend/src/Settings/MediaManagement/Naming/Naming.js
index a4a1d4b09..8d188551f 100644
--- a/frontend/src/Settings/MediaManagement/Naming/Naming.js
+++ b/frontend/src/Settings/MediaManagement/Naming/Naming.js
@@ -138,7 +138,8 @@ class Naming extends Component {
{ key: 1, value: translate('ReplaceWithDash') },
{ key: 2, value: translate('ReplaceWithSpaceDash') },
{ 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 = [];
@@ -262,6 +263,22 @@ class Naming extends Component {
null
}
+ {
+ replaceIllegalCharacters && settings.colonReplacementFormat.value === 5 ?
+
+ {translate('ColonReplacement')}
+
+
+ :
+ null
+ }
+
{
renameEpisodes &&
diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/ColonReplacementFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/ColonReplacementFixture.cs
index ae0ae7297..18b4aa9f1 100644
--- a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/ColonReplacementFixture.cs
+++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/ColonReplacementFixture.cs
@@ -90,5 +90,18 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile)
.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 { _episode1 }, _series, _episodeFile)
+ .Should().Be(expected);
+ }
}
}
diff --git a/src/NzbDrone.Core/Datastore/Migration/211_add_custom_colon_replacement_to_naming_config.cs b/src/NzbDrone.Core/Datastore/Migration/211_add_custom_colon_replacement_to_naming_config.cs
new file mode 100644
index 000000000..537a70ea9
--- /dev/null
+++ b/src/NzbDrone.Core/Datastore/Migration/211_add_custom_colon_replacement_to_naming_config.cs
@@ -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("");
+ }
+ }
+}
diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json
index 36e3c35c1..76d905d4e 100644
--- a/src/NzbDrone.Core/Localization/Core/en.json
+++ b/src/NzbDrone.Core/Localization/Core/en.json
@@ -270,6 +270,9 @@
"CreateGroup": "Create Group",
"CurrentlyInstalled": "Currently Installed",
"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",
"CustomFilters": "Custom Filters",
"CustomFormat": "Custom Format",
diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs
index 714605a99..3755f17f7 100644
--- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs
+++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs
@@ -6,7 +6,6 @@ using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Diacritical;
-using DryIoc.ImTools;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Disk;
@@ -112,6 +111,9 @@ namespace NzbDrone.Core.Organizer
{ "wel", "cym" }
}.ToImmutableDictionary();
+ public static readonly ImmutableArray BadCharacters = ImmutableArray.Create("\\", "/", "<", ">", "?", "*", "|", "\"");
+ public static readonly ImmutableArray GoodCharacters = ImmutableArray.Create("+", "+", "", "", "!", "-", "", "");
+
public FileNameBuilder(INamingConfigService namingConfigService,
IQualityDefinitionService qualityDefinitionService,
ICacheManager cacheManager,
@@ -1156,8 +1158,6 @@ namespace NzbDrone.Core.Organizer
private static string CleanFileName(string name, NamingConfig namingConfig)
{
var result = name;
- string[] badCharacters = { "\\", "/", "<", ">", "?", "*", "|", "\"" };
- string[] goodCharacters = { "+", "+", "", "", "!", "-", "", "" };
if (namingConfig.ReplaceIllegalCharacters)
{
@@ -1182,6 +1182,9 @@ namespace NzbDrone.Core.Organizer
case ColonReplacementFormat.SpaceDashSpace:
replacement = " - ";
break;
+ case ColonReplacementFormat.Custom:
+ replacement = namingConfig.CustomColonReplacementFormat;
+ break;
}
result = result.Replace(":", replacement);
@@ -1192,9 +1195,9 @@ namespace NzbDrone.Core.Organizer
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(' ');
@@ -1268,6 +1271,7 @@ namespace NzbDrone.Core.Organizer
Dash = 1,
SpaceDash = 2,
SpaceDashSpace = 3,
- Smart = 4
+ Smart = 4,
+ Custom = 5
}
}
diff --git a/src/NzbDrone.Core/Organizer/FileNameValidation.cs b/src/NzbDrone.Core/Organizer/FileNameValidation.cs
index e8d39469f..b36f9426c 100644
--- a/src/NzbDrone.Core/Organizer/FileNameValidation.cs
+++ b/src/NzbDrone.Core/Organizer/FileNameValidation.cs
@@ -1,3 +1,4 @@
+using System;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
@@ -61,6 +62,13 @@ namespace NzbDrone.Core.Organizer
return ruleBuilder.SetValidator(new IllegalCharactersValidator());
}
+
+ public static IRuleBuilderOptions ValidCustomColonReplacement(this IRuleBuilder ruleBuilder)
+ {
+ ruleBuilder.SetValidator(new IllegalColonCharactersValidator());
+
+ return ruleBuilder.SetValidator(new IllegalCharactersValidator());
+ }
}
public class ValidStandardEpisodeFormatValidator : PropertyValidator
@@ -132,6 +140,34 @@ namespace NzbDrone.Core.Organizer
}
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())
{
context.MessageFormatter.AppendArgument("InvalidCharacters", string.Join("", invalidCharacters));
diff --git a/src/NzbDrone.Core/Organizer/NamingConfig.cs b/src/NzbDrone.Core/Organizer/NamingConfig.cs
index 57d88d405..eb3f65c84 100644
--- a/src/NzbDrone.Core/Organizer/NamingConfig.cs
+++ b/src/NzbDrone.Core/Organizer/NamingConfig.cs
@@ -9,6 +9,7 @@ namespace NzbDrone.Core.Organizer
RenameEpisodes = false,
ReplaceIllegalCharacters = true,
ColonReplacementFormat = ColonReplacementFormat.Smart,
+ CustomColonReplacementFormat = string.Empty,
MultiEpisodeStyle = MultiEpisodeStyle.PrefixedRange,
StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {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 ReplaceIllegalCharacters { get; set; }
public ColonReplacementFormat ColonReplacementFormat { get; set; }
+ public string CustomColonReplacementFormat { get; set; }
public MultiEpisodeStyle MultiEpisodeStyle { get; set; }
public string StandardEpisodeFormat { get; set; }
public string DailyEpisodeFormat { get; set; }
diff --git a/src/Sonarr.Api.V3/Config/NamingConfigController.cs b/src/Sonarr.Api.V3/Config/NamingConfigController.cs
index a05894f45..f8870f35c 100644
--- a/src/Sonarr.Api.V3/Config/NamingConfigController.cs
+++ b/src/Sonarr.Api.V3/Config/NamingConfigController.cs
@@ -36,6 +36,7 @@ namespace Sonarr.Api.V3.Config
SharedValidator.RuleFor(c => c.SeriesFolderFormat).ValidSeriesFolderFormat();
SharedValidator.RuleFor(c => c.SeasonFolderFormat).ValidSeasonFolderFormat();
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)
diff --git a/src/Sonarr.Api.V3/Config/NamingConfigResource.cs b/src/Sonarr.Api.V3/Config/NamingConfigResource.cs
index f1acc9fe3..e3354fb38 100644
--- a/src/Sonarr.Api.V3/Config/NamingConfigResource.cs
+++ b/src/Sonarr.Api.V3/Config/NamingConfigResource.cs
@@ -7,6 +7,7 @@ namespace Sonarr.Api.V3.Config
public bool RenameEpisodes { get; set; }
public bool ReplaceIllegalCharacters { get; set; }
public int ColonReplacementFormat { get; set; }
+ public string CustomColonReplacementFormat { get; set; }
public int MultiEpisodeStyle { get; set; }
public string StandardEpisodeFormat { get; set; }
public string DailyEpisodeFormat { get; set; }
diff --git a/src/Sonarr.Api.V3/Config/NamingExampleResource.cs b/src/Sonarr.Api.V3/Config/NamingExampleResource.cs
index 6784e1a4b..ed1bfbd92 100644
--- a/src/Sonarr.Api.V3/Config/NamingExampleResource.cs
+++ b/src/Sonarr.Api.V3/Config/NamingExampleResource.cs
@@ -25,6 +25,7 @@ namespace Sonarr.Api.V3.Config
RenameEpisodes = model.RenameEpisodes,
ReplaceIllegalCharacters = model.ReplaceIllegalCharacters,
ColonReplacementFormat = (int)model.ColonReplacementFormat,
+ CustomColonReplacementFormat = model.CustomColonReplacementFormat,
MultiEpisodeStyle = (int)model.MultiEpisodeStyle,
StandardEpisodeFormat = model.StandardEpisodeFormat,
DailyEpisodeFormat = model.DailyEpisodeFormat,
@@ -45,6 +46,7 @@ namespace Sonarr.Api.V3.Config
ReplaceIllegalCharacters = resource.ReplaceIllegalCharacters,
MultiEpisodeStyle = (MultiEpisodeStyle)resource.MultiEpisodeStyle,
ColonReplacementFormat = (ColonReplacementFormat)resource.ColonReplacementFormat,
+ CustomColonReplacementFormat = resource.CustomColonReplacementFormat,
StandardEpisodeFormat = resource.StandardEpisodeFormat,
DailyEpisodeFormat = resource.DailyEpisodeFormat,
AnimeEpisodeFormat = resource.AnimeEpisodeFormat,