New: Colon replacement naming option
This commit is contained in:
parent
d3ad970ecc
commit
b3260ba866
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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; }
|
||||||
|
|
|
@ -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; }
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue