New: Colon replacement naming option

This commit is contained in:
Mark McDowall 2023-04-23 21:15:27 -07:00
parent d3ad970ecc
commit b3260ba866
7 changed files with 207 additions and 20 deletions

View File

@ -120,6 +120,7 @@ class Naming extends Component {
} = this.state; } = this.state;
const renameEpisodes = hasSettings && settings.renameEpisodes.value; const renameEpisodes = hasSettings && settings.renameEpisodes.value;
const replaceIllegalCharacters = hasSettings && settings.replaceIllegalCharacters.value;
const multiEpisodeStyleOptions = [ const multiEpisodeStyleOptions = [
{ key: 0, value: 'Extend', hint: 'S01E01-02-03' }, { key: 0, value: 'Extend', hint: 'S01E01-02-03' },
@ -130,6 +131,14 @@ class Naming extends Component {
{ key: 5, value: 'Prefixed Range', hint: 'S01E01-E03' } { 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 standardEpisodeFormatHelpTexts = [];
const standardEpisodeFormatErrors = []; const standardEpisodeFormatErrors = [];
const dailyEpisodeFormatHelpTexts = []; const dailyEpisodeFormatHelpTexts = [];
@ -232,6 +241,22 @@ class Naming extends Component {
/> />
</FormGroup> </FormGroup>
{
replaceIllegalCharacters ?
<FormGroup>
<FormLabel>Colon Replacement</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="colonReplacementFormat"
values={colonReplacementOptions}
onChange={onInputChange}
{...settings.colonReplacementFormat}
/>
</FormGroup> :
null
}
{ {
renameEpisodes && renameEpisodes &&
<div> <div>

View File

@ -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<FileNameBuilder>
{
private Series _series;
private Episode _episode1;
private EpisodeFile _episodeFile;
private NamingConfig _namingConfig;
[SetUp]
public void Setup()
{
_series = Builder<Series>
.CreateNew()
.With(s => s.Title = "CSI: Vegas")
.Build();
_namingConfig = NamingConfig.Default;
_namingConfig.RenameEpisodes = true;
Mocker.GetMock<INamingConfigService>()
.Setup(c => c.GetConfig()).Returns(_namingConfig);
_episode1 = Builder<Episode>.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<IQualityDefinitionService>()
.Setup(v => v.Get(Moq.It.IsAny<Quality>()))
.Returns<Quality>(v => Quality.DefaultQualityDefinitions.First(c => c.Quality == v));
Mocker.GetMock<ICustomFormatService>()
.Setup(v => v.All())
.Returns(new List<CustomFormat>());
}
[Test]
public void should_replace_colon_followed_by_space_with_space_dash_space_by_default()
{
_namingConfig.StandardEpisodeFormat = "{Series Title}";
Subject.BuildFileName(new List<Episode> { _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<Episode> { _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<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(189)]
public class add_colon_replacement_to_naming_config : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Alter.Table("NamingConfig").AddColumn("ColonReplacementFormat").AsInt32().WithDefaultValue(4);
}
}
}

View File

@ -377,30 +377,17 @@ namespace NzbDrone.Core.Organizer
return title; return title;
} }
public static string CleanFileName(string name, bool replace = true) public static string CleanFileName(string name)
{ {
string result = name; return CleanFileName(name, NamingConfig.Default);
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(' ');
} }
public static string CleanFolderName(string name) public static string CleanFolderName(string name)
{ {
name = FileNameCleanupRegex.Replace(name, match => match.Captures[0].Value[0].ToString()); 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<Episode> episodes) public bool RequiresEpisodeTitle(Series series, List<Episode> episodes)
@ -867,7 +854,7 @@ namespace NzbDrone.Core.Organizer
replacementText = replacementText.Replace(" ", tokenMatch.Separator); replacementText = replacementText.Replace(" ", tokenMatch.Separator);
} }
replacementText = CleanFileName(replacementText, namingConfig.ReplaceIllegalCharacters); replacementText = CleanFileName(replacementText, namingConfig);
if (!replacementText.IsNullOrWhiteSpace()) if (!replacementText.IsNullOrWhiteSpace())
{ {
@ -1117,6 +1104,53 @@ namespace NzbDrone.Core.Organizer
// Replace reserved windows device names with an alternative // Replace reserved windows device names with an alternative
return ReservedDeviceNamesRegex.Replace(input, match => match.Value.Replace(".", "_")); 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 internal sealed class TokenMatch
@ -1150,4 +1184,13 @@ namespace NzbDrone.Core.Organizer
Range = 4, Range = 4,
PrefixedRange = 5 PrefixedRange = 5
} }
public enum ColonReplacementFormat
{
Delete = 0,
Dash = 1,
SpaceDash = 2,
SpaceDashSpace = 3,
Smart = 4
}
} }

View File

@ -8,6 +8,7 @@ namespace NzbDrone.Core.Organizer
{ {
RenameEpisodes = false, RenameEpisodes = false,
ReplaceIllegalCharacters = true, ReplaceIllegalCharacters = true,
ColonReplacementFormat = ColonReplacementFormat.Smart,
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}",
@ -19,6 +20,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 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

@ -1,4 +1,4 @@
using Sonarr.Http.REST; using Sonarr.Http.REST;
namespace Sonarr.Api.V3.Config namespace Sonarr.Api.V3.Config
{ {
@ -6,6 +6,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 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

@ -24,6 +24,7 @@ namespace Sonarr.Api.V3.Config
RenameEpisodes = model.RenameEpisodes, RenameEpisodes = model.RenameEpisodes,
ReplaceIllegalCharacters = model.ReplaceIllegalCharacters, ReplaceIllegalCharacters = model.ReplaceIllegalCharacters,
ColonReplacementFormat = (int)model.ColonReplacementFormat,
MultiEpisodeStyle = (int)model.MultiEpisodeStyle, MultiEpisodeStyle = (int)model.MultiEpisodeStyle,
StandardEpisodeFormat = model.StandardEpisodeFormat, StandardEpisodeFormat = model.StandardEpisodeFormat,
DailyEpisodeFormat = model.DailyEpisodeFormat, DailyEpisodeFormat = model.DailyEpisodeFormat,
@ -60,6 +61,7 @@ namespace Sonarr.Api.V3.Config
RenameEpisodes = resource.RenameEpisodes, RenameEpisodes = resource.RenameEpisodes,
ReplaceIllegalCharacters = resource.ReplaceIllegalCharacters, ReplaceIllegalCharacters = resource.ReplaceIllegalCharacters,
MultiEpisodeStyle = (MultiEpisodeStyle)resource.MultiEpisodeStyle, MultiEpisodeStyle = (MultiEpisodeStyle)resource.MultiEpisodeStyle,
ColonReplacementFormat = (ColonReplacementFormat)resource.ColonReplacementFormat,
StandardEpisodeFormat = resource.StandardEpisodeFormat, StandardEpisodeFormat = resource.StandardEpisodeFormat,
DailyEpisodeFormat = resource.DailyEpisodeFormat, DailyEpisodeFormat = resource.DailyEpisodeFormat,
AnimeEpisodeFormat = resource.AnimeEpisodeFormat, AnimeEpisodeFormat = resource.AnimeEpisodeFormat,