diff --git a/frontend/src/Settings/MediaManagement/Naming/Naming.js b/frontend/src/Settings/MediaManagement/Naming/Naming.js
index 1cd6c16c7..5f7ccf19a 100644
--- a/frontend/src/Settings/MediaManagement/Naming/Naming.js
+++ b/frontend/src/Settings/MediaManagement/Naming/Naming.js
@@ -120,6 +120,7 @@ class Naming extends Component {
} = this.state;
const renameEpisodes = hasSettings && settings.renameEpisodes.value;
+ const replaceIllegalCharacters = hasSettings && settings.replaceIllegalCharacters.value;
const multiEpisodeStyleOptions = [
{ key: 0, value: 'Extend', hint: 'S01E01-02-03' },
@@ -130,6 +131,14 @@ class Naming extends Component {
{ key: 5, value: 'Prefixed Range', hint: 'S01E01-E03' }
];
+ const colonReplacementOptions = [
+ { key: 0, value: 'Delete' },
+ { key: 1, value: 'Replace with Dash' },
+ { key: 2, value: 'Replace with Space Dash' },
+ { key: 3, value: 'Replace with Space Dash Space' },
+ { key: 4, value: 'Smart Replace', hint: 'Dash or Space Dash depending on name' }
+ ];
+
const standardEpisodeFormatHelpTexts = [];
const standardEpisodeFormatErrors = [];
const dailyEpisodeFormatHelpTexts = [];
@@ -232,6 +241,22 @@ class Naming extends Component {
/>
+ {
+ replaceIllegalCharacters ?
+
+ Colon Replacement
+
+
+ :
+ null
+ }
+
{
renameEpisodes &&
diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/ColonReplacementFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/ColonReplacementFixture.cs
new file mode 100644
index 000000000..6afc72fe2
--- /dev/null
+++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/ColonReplacementFixture.cs
@@ -0,0 +1,100 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using FizzWare.NBuilder;
+using FluentAssertions;
+using Moq;
+using NUnit.Framework;
+using NzbDrone.Core.CustomFormats;
+using NzbDrone.Core.MediaFiles;
+using NzbDrone.Core.MediaFiles.MediaInfo;
+using NzbDrone.Core.Organizer;
+using NzbDrone.Core.Qualities;
+using NzbDrone.Core.Test.Framework;
+using NzbDrone.Core.Tv;
+using NzbDrone.Test.Common;
+
+namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
+{
+ [TestFixture]
+ public class ColonReplacementFixture : CoreTest
+ {
+ private Series _series;
+ private Episode _episode1;
+ private EpisodeFile _episodeFile;
+ private NamingConfig _namingConfig;
+
+ [SetUp]
+ public void Setup()
+ {
+ _series = Builder
+ .CreateNew()
+ .With(s => s.Title = "CSI: Vegas")
+ .Build();
+
+ _namingConfig = NamingConfig.Default;
+ _namingConfig.RenameEpisodes = true;
+
+ Mocker.GetMock()
+ .Setup(c => c.GetConfig()).Returns(_namingConfig);
+
+ _episode1 = Builder.CreateNew()
+ .With(e => e.Title = "What Happens in Vegas")
+ .With(e => e.SeasonNumber = 1)
+ .With(e => e.EpisodeNumber = 6)
+ .With(e => e.AbsoluteEpisodeNumber = 100)
+ .Build();
+
+ _episodeFile = new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p), ReleaseGroup = "SonarrTest" };
+
+ Mocker.GetMock()
+ .Setup(v => v.Get(Moq.It.IsAny()))
+ .Returns(v => Quality.DefaultQualityDefinitions.First(c => c.Quality == v));
+
+ Mocker.GetMock()
+ .Setup(v => v.All())
+ .Returns(new List());
+ }
+
+ [Test]
+ public void should_replace_colon_followed_by_space_with_space_dash_space_by_default()
+ {
+ _namingConfig.StandardEpisodeFormat = "{Series Title}";
+
+ Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile)
+ .Should().Be("CSI - Vegas");
+ }
+
+ [TestCase("CSI: Vegas", ColonReplacementFormat.Smart, "CSI - Vegas")]
+ [TestCase("CSI: Vegas", ColonReplacementFormat.Dash, "CSI- Vegas")]
+ [TestCase("CSI: Vegas", ColonReplacementFormat.Delete, "CSI Vegas")]
+ [TestCase("CSI: Vegas", ColonReplacementFormat.SpaceDash, "CSI - Vegas")]
+ [TestCase("CSI: Vegas", ColonReplacementFormat.SpaceDashSpace, "CSI - Vegas")]
+ public void should_replace_colon_followed_by_space_with_expected_result(string seriesName, ColonReplacementFormat replacementFormat, string expected)
+ {
+ _series.Title = seriesName;
+ _namingConfig.StandardEpisodeFormat = "{Series Title}";
+ _namingConfig.ColonReplacementFormat = replacementFormat;
+
+ Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile)
+ .Should().Be(expected);
+ }
+
+ [TestCase("Series:Title", ColonReplacementFormat.Smart, "Series-Title")]
+ [TestCase("Series:Title", ColonReplacementFormat.Dash, "Series-Title")]
+ [TestCase("Series:Title", ColonReplacementFormat.Delete, "SeriesTitle")]
+ [TestCase("Series:Title", ColonReplacementFormat.SpaceDash, "Series -Title")]
+ [TestCase("Series:Title", ColonReplacementFormat.SpaceDashSpace, "Series - Title")]
+ public void should_replace_colon_with_expected_result(string seriesName, ColonReplacementFormat replacementFormat, string expected)
+ {
+ _series.Title = seriesName;
+ _namingConfig.StandardEpisodeFormat = "{Series Title}";
+ _namingConfig.ColonReplacementFormat = replacementFormat;
+
+ Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile)
+ .Should().Be(expected);
+ }
+ }
+}
diff --git a/src/NzbDrone.Core/Datastore/Migration/189_add_colon_replacement_to_naming_config.cs b/src/NzbDrone.Core/Datastore/Migration/189_add_colon_replacement_to_naming_config.cs
new file mode 100644
index 000000000..e669e4f36
--- /dev/null
+++ b/src/NzbDrone.Core/Datastore/Migration/189_add_colon_replacement_to_naming_config.cs
@@ -0,0 +1,14 @@
+using FluentMigrator;
+using NzbDrone.Core.Datastore.Migration.Framework;
+
+namespace NzbDrone.Core.Datastore.Migration
+{
+ [Migration(189)]
+ public class add_colon_replacement_to_naming_config : NzbDroneMigrationBase
+ {
+ protected override void MainDbUpgrade()
+ {
+ Alter.Table("NamingConfig").AddColumn("ColonReplacementFormat").AsInt32().WithDefaultValue(4);
+ }
+ }
+}
diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs
index 9b4d96a84..364296348 100644
--- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs
+++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs
@@ -377,30 +377,17 @@ namespace NzbDrone.Core.Organizer
return title;
}
- public static string CleanFileName(string name, bool replace = true)
+ public static string CleanFileName(string name)
{
- string result = name;
- string[] badCharacters = { "\\", "/", "<", ">", "?", "*", ":", "|", "\"" };
- string[] goodCharacters = { "+", "+", "", "", "!", "-", "-", "", "" };
-
- // Replace a colon followed by a space with space dash space for a better appearance
- if (replace)
- {
- result = result.Replace(": ", " - ");
- }
-
- for (int i = 0; i < badCharacters.Length; i++)
- {
- result = result.Replace(badCharacters[i], replace ? goodCharacters[i] : string.Empty);
- }
-
- return result.TrimStart(' ', '.').TrimEnd(' ');
+ return CleanFileName(name, NamingConfig.Default);
}
public static string CleanFolderName(string name)
{
name = FileNameCleanupRegex.Replace(name, match => match.Captures[0].Value[0].ToString());
- return name.Trim(' ', '.');
+ name = name.Trim(' ', '.');
+
+ return CleanFileName(name);
}
public bool RequiresEpisodeTitle(Series series, List episodes)
@@ -867,7 +854,7 @@ namespace NzbDrone.Core.Organizer
replacementText = replacementText.Replace(" ", tokenMatch.Separator);
}
- replacementText = CleanFileName(replacementText, namingConfig.ReplaceIllegalCharacters);
+ replacementText = CleanFileName(replacementText, namingConfig);
if (!replacementText.IsNullOrWhiteSpace())
{
@@ -1117,6 +1104,53 @@ namespace NzbDrone.Core.Organizer
// Replace reserved windows device names with an alternative
return ReservedDeviceNamesRegex.Replace(input, match => match.Value.Replace(".", "_"));
}
+
+ private static string CleanFileName(string name, NamingConfig namingConfig)
+ {
+ var result = name;
+ string[] badCharacters = { "\\", "/", "<", ">", "?", "*", "|", "\"" };
+ string[] goodCharacters = { "+", "+", "", "", "!", "-", "", "" };
+
+ if (namingConfig.ReplaceIllegalCharacters)
+ {
+ // Smart replaces a colon followed by a space with space dash space for a better appearance
+ if (namingConfig.ColonReplacementFormat == ColonReplacementFormat.Smart)
+ {
+ result = result.Replace(": ", " - ");
+ result = result.Replace(":", "-");
+ }
+ else
+ {
+ var replacement = string.Empty;
+
+ switch (namingConfig.ColonReplacementFormat)
+ {
+ case ColonReplacementFormat.Dash:
+ replacement = "-";
+ break;
+ case ColonReplacementFormat.SpaceDash:
+ replacement = " -";
+ break;
+ case ColonReplacementFormat.SpaceDashSpace:
+ replacement = " - ";
+ break;
+ }
+
+ result = result.Replace(":", replacement);
+ }
+ }
+ else
+ {
+ result = result.Replace(":", string.Empty);
+ }
+
+ for (var i = 0; i < badCharacters.Length; i++)
+ {
+ result = result.Replace(badCharacters[i], namingConfig.ReplaceIllegalCharacters ? goodCharacters[i] : string.Empty);
+ }
+
+ return result.TrimStart(' ', '.').TrimEnd(' ');
+ }
}
internal sealed class TokenMatch
@@ -1150,4 +1184,13 @@ namespace NzbDrone.Core.Organizer
Range = 4,
PrefixedRange = 5
}
+
+ public enum ColonReplacementFormat
+ {
+ Delete = 0,
+ Dash = 1,
+ SpaceDash = 2,
+ SpaceDashSpace = 3,
+ Smart = 4
+ }
}
diff --git a/src/NzbDrone.Core/Organizer/NamingConfig.cs b/src/NzbDrone.Core/Organizer/NamingConfig.cs
index 50f5d254c..57d88d405 100644
--- a/src/NzbDrone.Core/Organizer/NamingConfig.cs
+++ b/src/NzbDrone.Core/Organizer/NamingConfig.cs
@@ -8,6 +8,7 @@ namespace NzbDrone.Core.Organizer
{
RenameEpisodes = false,
ReplaceIllegalCharacters = true,
+ ColonReplacementFormat = ColonReplacementFormat.Smart,
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}",
@@ -19,6 +20,7 @@ namespace NzbDrone.Core.Organizer
public bool RenameEpisodes { get; set; }
public bool ReplaceIllegalCharacters { get; set; }
+ public ColonReplacementFormat ColonReplacementFormat { 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/NamingConfigResource.cs b/src/Sonarr.Api.V3/Config/NamingConfigResource.cs
index 8b95058a7..4e4116807 100644
--- a/src/Sonarr.Api.V3/Config/NamingConfigResource.cs
+++ b/src/Sonarr.Api.V3/Config/NamingConfigResource.cs
@@ -1,4 +1,4 @@
-using Sonarr.Http.REST;
+using Sonarr.Http.REST;
namespace Sonarr.Api.V3.Config
{
@@ -6,6 +6,7 @@ namespace Sonarr.Api.V3.Config
{
public bool RenameEpisodes { get; set; }
public bool ReplaceIllegalCharacters { get; set; }
+ public int ColonReplacementFormat { 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 103609d5d..66e21aff8 100644
--- a/src/Sonarr.Api.V3/Config/NamingExampleResource.cs
+++ b/src/Sonarr.Api.V3/Config/NamingExampleResource.cs
@@ -24,6 +24,7 @@ namespace Sonarr.Api.V3.Config
RenameEpisodes = model.RenameEpisodes,
ReplaceIllegalCharacters = model.ReplaceIllegalCharacters,
+ ColonReplacementFormat = (int)model.ColonReplacementFormat,
MultiEpisodeStyle = (int)model.MultiEpisodeStyle,
StandardEpisodeFormat = model.StandardEpisodeFormat,
DailyEpisodeFormat = model.DailyEpisodeFormat,
@@ -60,6 +61,7 @@ namespace Sonarr.Api.V3.Config
RenameEpisodes = resource.RenameEpisodes,
ReplaceIllegalCharacters = resource.ReplaceIllegalCharacters,
MultiEpisodeStyle = (MultiEpisodeStyle)resource.MultiEpisodeStyle,
+ ColonReplacementFormat = (ColonReplacementFormat)resource.ColonReplacementFormat,
StandardEpisodeFormat = resource.StandardEpisodeFormat,
DailyEpisodeFormat = resource.DailyEpisodeFormat,
AnimeEpisodeFormat = resource.AnimeEpisodeFormat,