diff --git a/frontend/src/Settings/MediaManagement/Naming/Naming.js b/frontend/src/Settings/MediaManagement/Naming/Naming.js index 8879a8b47..11d48366b 100644 --- a/frontend/src/Settings/MediaManagement/Naming/Naming.js +++ b/frontend/src/Settings/MediaManagement/Naming/Naming.js @@ -87,6 +87,15 @@ class Naming extends Component { } = this.state; const renameTracks = hasSettings && settings.renameTracks.value; + const replaceIllegalCharacters = hasSettings && settings.replaceIllegalCharacters.value; + + const colonReplacementOptions = [ + { key: 0, value: translate('Delete') }, + { key: 1, value: translate('ReplaceWithDash') }, + { key: 2, value: translate('ReplaceWithSpaceDash') }, + { key: 3, value: translate('ReplaceWithSpaceDashSpace') }, + { key: 4, value: translate('SmartReplace'), hint: translate('DashOrSpaceDashDependingOnName') } + ]; const standardTrackFormatHelpTexts = []; const standardTrackFormatErrors = []; @@ -160,6 +169,24 @@ class Naming extends Component { /> + { + replaceIllegalCharacters ? + + + {translate('ColonReplacement')} + + + + : + null + } + { renameTracks &&
diff --git a/src/Lidarr.Api.V1/Config/NamingConfigResource.cs b/src/Lidarr.Api.V1/Config/NamingConfigResource.cs index ed00d6b78..1c1f7ba2f 100644 --- a/src/Lidarr.Api.V1/Config/NamingConfigResource.cs +++ b/src/Lidarr.Api.V1/Config/NamingConfigResource.cs @@ -6,6 +6,7 @@ namespace Lidarr.Api.V1.Config { public bool RenameTracks { get; set; } public bool ReplaceIllegalCharacters { get; set; } + public int ColonReplacementFormat { get; set; } public string StandardTrackFormat { get; set; } public string MultiDiscTrackFormat { get; set; } public string ArtistFolderFormat { get; set; } diff --git a/src/Lidarr.Api.V1/Config/NamingExampleResource.cs b/src/Lidarr.Api.V1/Config/NamingExampleResource.cs index ad587b431..19319d5e2 100644 --- a/src/Lidarr.Api.V1/Config/NamingExampleResource.cs +++ b/src/Lidarr.Api.V1/Config/NamingExampleResource.cs @@ -20,6 +20,7 @@ namespace Lidarr.Api.V1.Config RenameTracks = model.RenameTracks, ReplaceIllegalCharacters = model.ReplaceIllegalCharacters, + ColonReplacementFormat = (int)model.ColonReplacementFormat, StandardTrackFormat = model.StandardTrackFormat, MultiDiscTrackFormat = model.MultiDiscTrackFormat, ArtistFolderFormat = model.ArtistFolderFormat @@ -44,6 +45,7 @@ namespace Lidarr.Api.V1.Config RenameTracks = resource.RenameTracks, ReplaceIllegalCharacters = resource.ReplaceIllegalCharacters, + ColonReplacementFormat = (ColonReplacementFormat)resource.ColonReplacementFormat, StandardTrackFormat = resource.StandardTrackFormat, MultiDiscTrackFormat = resource.MultiDiscTrackFormat, 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..b214b395d --- /dev/null +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/ColonReplacementFixture.cs @@ -0,0 +1,105 @@ +using System.Collections.Generic; +using System.Linq; +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.CustomFormats; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Music; +using NzbDrone.Core.Organizer; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests +{ + [TestFixture] + public class ColonReplacementFixture : CoreTest + { + private Artist _artist; + private Album _album; + private AlbumRelease _release; + private Track _track; + private TrackFile _trackFile; + private NamingConfig _namingConfig; + + [SetUp] + public void Setup() + { + _artist = Builder + .CreateNew() + .With(s => s.Name = "Nu:Tone") + .Build(); + + _album = Builder + .CreateNew() + .With(s => s.Title = "Medical History") + .Build(); + + _release = Builder + .CreateNew() + .With(s => s.Media = new List { new () { Number = 14 } }) + .Build(); + + _track = Builder.CreateNew() + .With(e => e.Title = "System: Accapella") + .With(e => e.AbsoluteTrackNumber = 14) + .With(e => e.AlbumRelease = _release) + .Build(); + + _trackFile = new TrackFile { Quality = new QualityModel(Quality.MP3_256), ReleaseGroup = "LidarrTest" }; + + _namingConfig = NamingConfig.Default; + _namingConfig.RenameTracks = true; + + Mocker.GetMock() + .Setup(c => c.GetConfig()).Returns(_namingConfig); + + 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.StandardTrackFormat = "{Artist Name} - {Album Title} - {Track Title}"; + + Subject.BuildTrackFileName(new List { _track }, _artist, _album, _trackFile) + .Should().Be("Nu-Tone - Medical History - System - Accapella"); + } + + [TestCase("System: Accapella", ColonReplacementFormat.Smart, "Nu-Tone - Medical History - System - Accapella")] + [TestCase("System: Accapella", ColonReplacementFormat.Dash, "Nu-Tone - Medical History - System- Accapella")] + [TestCase("System: Accapella", ColonReplacementFormat.Delete, "NuTone - Medical History - System Accapella")] + [TestCase("System: Accapella", ColonReplacementFormat.SpaceDash, "Nu -Tone - Medical History - System - Accapella")] + [TestCase("System: Accapella", ColonReplacementFormat.SpaceDashSpace, "Nu - Tone - Medical History - System - Accapella")] + public void should_replace_colon_followed_by_space_with_expected_result(string trackTitle, ColonReplacementFormat replacementFormat, string expected) + { + _track.Title = trackTitle; + _namingConfig.StandardTrackFormat = "{Artist Name} - {Album Title} - {Track Title}"; + _namingConfig.ColonReplacementFormat = replacementFormat; + + Subject.BuildTrackFileName(new List { _track }, _artist, _album, _trackFile) + .Should().Be(expected); + } + + [TestCase("Artist:Name", ColonReplacementFormat.Smart, "Artist-Name")] + [TestCase("Artist:Name", ColonReplacementFormat.Dash, "Artist-Name")] + [TestCase("Artist:Name", ColonReplacementFormat.Delete, "ArtistName")] + [TestCase("Artist:Name", ColonReplacementFormat.SpaceDash, "Artist -Name")] + [TestCase("Artist:Name", ColonReplacementFormat.SpaceDashSpace, "Artist - Name")] + public void should_replace_colon_with_expected_result(string artistName, ColonReplacementFormat replacementFormat, string expected) + { + _artist.Name = artistName; + _namingConfig.StandardTrackFormat = "{Artist Name}"; + _namingConfig.ColonReplacementFormat = replacementFormat; + + Subject.BuildTrackFileName(new List { _track }, _artist, _album, _trackFile) + .Should().Be(expected); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/068_add_colon_replacement_to_naming_config.cs b/src/NzbDrone.Core/Datastore/Migration/068_add_colon_replacement_to_naming_config.cs new file mode 100644 index 000000000..d39ddb725 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/068_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(068)] + 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/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 09b3bfee5..785543d22 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -141,6 +141,7 @@ "Close": "Close", "CollapseMultipleAlbums": "Collapse Multiple Albums", "CollapseMultipleAlbumsHelpText": "Collapse multiple albums releasing on the same day", + "ColonReplacement": "Colon Replacement", "Columns": "Columns", "CombineWithExistingFiles": "Combine With Existing Files", "CompletedDownloadHandling": "Completed Download Handling", @@ -174,6 +175,7 @@ "CutoffHelpText": "Once this quality is reached Lidarr will no longer download albums", "CutoffUnmet": "Cutoff Unmet", "DBMigration": "DB Migration", + "DashOrSpaceDashDependingOnName": "Dash or Space Dash depending on name", "Database": "Database", "Date": "Date", "DateAdded": "Date Added", @@ -711,6 +713,9 @@ "ReplaceExistingFiles": "Replace Existing Files", "ReplaceIllegalCharacters": "Replace Illegal Characters", "ReplaceIllegalCharactersHelpText": "Replace illegal characters. If unchecked, Lidarr will remove them instead", + "ReplaceWithDash": "Replace with Dash", + "ReplaceWithSpaceDash": "Replace with Space Dash", + "ReplaceWithSpaceDashSpace": "Replace with Space Dash Space", "RequiredHelpText": "The release must contain at least one of these terms (case insensitive)", "RequiredPlaceHolder": "Add new restriction", "RequiresRestartToTakeEffect": "Requires restart to take effect", @@ -816,6 +821,7 @@ "SkipFreeSpaceCheckWhenImportingHelpText": "Use when Lidarr is unable to detect free space from your artist root folder", "SkipRedownload": "Skip Redownload", "SkipredownloadHelpText": "Prevents Lidarr from trying download alternative releases for the removed items", + "SmartReplace": "Smart Replace", "SomeResultsFiltered": "Some results are hidden by the applied filter", "SorryThatAlbumCannotBeFound": "Sorry, that album cannot be found.", "SorryThatArtistCannotBeFound": "Sorry, that artist cannot be found.", diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 54a0ee8ac..63329cac7 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -242,30 +242,17 @@ namespace NzbDrone.Core.Organizer return TitlePrefixRegex.Replace(title, "$2, $1$3"); } - 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.Trim(); + 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); } private void AddArtistTokens(Dictionary> tokenHandlers, Artist artist) @@ -423,7 +410,7 @@ namespace NzbDrone.Core.Organizer replacementText = replacementText.Replace(" ", tokenMatch.Separator); } - replacementText = CleanFileName(replacementText, namingConfig.ReplaceIllegalCharacters); + replacementText = CleanFileName(replacementText, namingConfig); if (!replacementText.IsNullOrWhiteSpace()) { @@ -551,6 +538,53 @@ namespace NzbDrone.Core.Organizer { return Path.GetFileNameWithoutExtension(trackFile.Path); } + + 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 @@ -584,4 +618,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 02760ad37..43d84d241 100644 --- a/src/NzbDrone.Core/Organizer/NamingConfig.cs +++ b/src/NzbDrone.Core/Organizer/NamingConfig.cs @@ -8,6 +8,7 @@ namespace NzbDrone.Core.Organizer { RenameTracks = false, ReplaceIllegalCharacters = true, + ColonReplacementFormat = ColonReplacementFormat.Smart, StandardTrackFormat = "{Album Title} ({Release Year})/{Artist Name} - {Album Title} - {track:00} - {Track Title}", MultiDiscTrackFormat = "{Album Title} ({Release Year})/{Medium Format} {medium:00}/{Artist Name} - {Album Title} - {track:00} - {Track Title}", ArtistFolderFormat = "{Artist Name}", @@ -15,6 +16,7 @@ namespace NzbDrone.Core.Organizer public bool RenameTracks { get; set; } public bool ReplaceIllegalCharacters { get; set; } + public ColonReplacementFormat ColonReplacementFormat { get; set; } public string StandardTrackFormat { get; set; } public string MultiDiscTrackFormat { get; set; } public string ArtistFolderFormat { get; set; }