diff --git a/frontend/src/Settings/MediaManagement/Naming/Naming.js b/frontend/src/Settings/MediaManagement/Naming/Naming.js index 1e13396bc..350c07b4e 100644 --- a/frontend/src/Settings/MediaManagement/Naming/Naming.js +++ b/frontend/src/Settings/MediaManagement/Naming/Naming.js @@ -13,33 +13,6 @@ import translate from 'Utilities/String/translate'; import NamingModal from './NamingModal'; import styles from './Naming.css'; -const colonReplacementOptions = [ - { - key: 'delete', - get value() { - return translate('Delete'); - } - }, - { - key: 'dash', - get value() { - return translate('ReplaceWithDash'); - } - }, - { - key: 'spaceDash', - get value() { - return translate('ReplaceWithSpaceDash'); - } - }, - { - key: 'spaceDashSpace', - get value() { - return translate('ReplaceWithSpaceDashSpace'); - } - } -]; - class Naming extends Component { // @@ -103,6 +76,14 @@ class Naming extends Component { const renameMovies = hasSettings && settings.renameMovies.value; const replaceIllegalCharacters = hasSettings && settings.replaceIllegalCharacters.value; + const colonReplacementOptions = [ + { key: 'delete', value: translate('Delete') }, + { key: 'dash', value: translate('ReplaceWithDash') }, + { key: 'spaceDash', value: translate('ReplaceWithSpaceDash') }, + { key: 'spaceDashSpace', value: translate('ReplaceWithSpaceDashSpace') }, + { key: 'smart', value: translate('SmartReplace'), hint: translate('SmartReplaceHint') } + ]; + const standardMovieFormatHelpTexts = []; const standardMovieFormatErrors = []; const movieFolderFormatHelpTexts = []; diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs index f6a540aca..fe782f0c9 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs @@ -299,7 +299,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests Subject.GetItems().Should().BeEmpty(); } - [TestCase("[ TOWN ]-[ http://www.town.ag ]-[ ANIME ]-[Usenet Provider >> http://www.ssl- <<] - [Commie] Aldnoah Zero 18 [234C8FC7]", "[ TOWN ]-[ http++www.town.ag ]-[ ANIME ]-[Usenet Provider http++www.ssl- ] - [Commie] Aldnoah Zero 18 [234C8FC7].nzb")] + [TestCase("[ TOWN ]-[ http://www.town.ag ]-[ ANIME ]-[Usenet Provider >> http://www.ssl- <<] - [Commie] Aldnoah Zero 18 [234C8FC7]", "[ TOWN ]-[ http-++www.town.ag ]-[ ANIME ]-[Usenet Provider http-++www.ssl- ] - [Commie] Aldnoah Zero 18 [234C8FC7].nzb")] public async Task Download_should_use_clean_title(string title, string filename) { GivenSuccessfulDownload(); diff --git a/src/NzbDrone.Core.Test/OrganizerTests/CleanFilenameFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/CleanFilenameFixture.cs new file mode 100644 index 000000000..19561438b --- /dev/null +++ b/src/NzbDrone.Core.Test/OrganizerTests/CleanFilenameFixture.cs @@ -0,0 +1,31 @@ +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Organizer; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.OrganizerTests +{ + [TestFixture] + public class CleanFilenameFixture : CoreTest + { + [TestCase("Mission: Impossible - no [HDTV-720p]", "Mission - Impossible - no [HDTV-720p]")] + public void should_replace_invalid_characters(string name, string expectedName) + { + FileNameBuilder.CleanFileName(name).Should().Be(expectedName); + } + + [TestCase(".45 (2006)", "45 (2006)")] + public void should_remove_periods_from_start(string name, string expectedName) + { + FileNameBuilder.CleanFileName(name).Should().Be(expectedName); + } + + [TestCase(" The Movie Title", "The Movie Title")] + [TestCase("The Movie Title ", "The Movie Title")] + [TestCase(" The Movie Title ", "The Movie Title")] + public void should_remove_spaces_from_start_and_end(string name, string expectedName) + { + FileNameBuilder.CleanFileName(name).Should().Be(expectedName); + } + } +} diff --git a/src/NzbDrone.Core.Test/OrganizerTests/CleanFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/CleanFixture.cs deleted file mode 100644 index 4b23a2c8b..000000000 --- a/src/NzbDrone.Core.Test/OrganizerTests/CleanFixture.cs +++ /dev/null @@ -1,20 +0,0 @@ -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.Organizer; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.OrganizerTests -{ - [TestFixture] - public class CleanFixture : CoreTest - { - [TestCase("Mission: Impossible - no [HDTV-720p]", - "Mission Impossible - no [HDTV-720p]")] - [TestCase(".45 (2006)", "45 (2006)")] - [TestCase(" The Movie Title ", "The Movie Title")] - public void CleanFileName(string name, string expectedName) - { - FileNameBuilder.CleanFileName(name).Should().Be(expectedName); - } - } -} 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..10fc75d8d --- /dev/null +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/ColonReplacementFixture.cs @@ -0,0 +1,86 @@ +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.Movies; +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 Movie _movie; + private MovieFile _movieFile; + private NamingConfig _namingConfig; + + [SetUp] + public void Setup() + { + _movie = Builder + .CreateNew() + .With(s => s.Title = "CSI: Vegas") + .Build(); + + _namingConfig = NamingConfig.Default; + _namingConfig.RenameMovies = true; + + Mocker.GetMock() + .Setup(c => c.GetConfig()).Returns(_namingConfig); + + _movieFile = new MovieFile { Quality = new QualityModel(Quality.HDTV720p), ReleaseGroup = "RadarrTest" }; + + 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.StandardMovieFormat = "{Movie Title}"; + + Subject.BuildFileName(_movie, _movieFile) + .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 movieName, ColonReplacementFormat replacementFormat, string expected) + { + _movie.Title = movieName; + _namingConfig.StandardMovieFormat = "{Movie Title}"; + _namingConfig.ColonReplacementFormat = replacementFormat; + + Subject.BuildFileName(_movie, _movieFile) + .Should().Be(expected); + } + + [TestCase("Movie:Title", ColonReplacementFormat.Smart, "Movie-Title")] + [TestCase("Movie:Title", ColonReplacementFormat.Dash, "Movie-Title")] + [TestCase("Movie:Title", ColonReplacementFormat.Delete, "MovieTitle")] + [TestCase("Movie:Title", ColonReplacementFormat.SpaceDash, "Movie -Title")] + [TestCase("Movie:Title", ColonReplacementFormat.SpaceDashSpace, "Movie - Title")] + public void should_replace_colon_with_expected_result(string movieName, ColonReplacementFormat replacementFormat, string expected) + { + _movie.Title = movieName; + _namingConfig.StandardMovieFormat = "{Movie Title}"; + _namingConfig.ColonReplacementFormat = replacementFormat; + + Subject.BuildFileName(_movie, _movieFile) + .Should().Be(expected); + } + } +} diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/TitleTheFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/TitleTheFixture.cs index dfbabd09a..556f21b4b 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/TitleTheFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/TitleTheFixture.cs @@ -53,7 +53,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests [TestCase("The Sixth Sense 2 (Thai)", "Sixth Sense 2, The (Thai)")] [TestCase("The Amazing Race (Latin America)", "Amazing Race, The (Latin America)")] [TestCase("The Rat Pack (A&E)", "Rat Pack, The (A&E)")] - [TestCase("The Climax: I (Almost) Got Away With It (2016)", "Climax I (Almost) Got Away With It, The (2016)")] + [TestCase("The Climax: I (Almost) Got Away With It (2016)", "Climax - I (Almost) Got Away With It, The (2016)")] public void should_get_expected_title_back(string title, string expected) { _movie.Title = title; diff --git a/src/NzbDrone.Core/Datastore/Migration/240_drop_multi_episode_style_from_naming_config.cs b/src/NzbDrone.Core/Datastore/Migration/240_drop_multi_episode_style_from_naming_config.cs new file mode 100644 index 000000000..47d77ba83 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/240_drop_multi_episode_style_from_naming_config.cs @@ -0,0 +1,14 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(240)] + public class drop_multi_episode_style_from_naming_config : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Delete.Column("MultiEpisodeStyle").FromTable("NamingConfig"); + } + } +} diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 45c71b584..884c7394c 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1634,6 +1634,8 @@ "SkipRedownload": "Skip Redownload", "SkipRedownloadHelpText": "Prevents {appName} from trying to download an alternative release for this item", "Small": "Small", + "SmartReplace": "Smart Replace", + "SmartReplaceHint": "Dash or Space Dash depending on name", "Socks4": "Socks4", "Socks5": "Socks5 (Support TOR)", "SomeResultsHiddenFilter": "Some results are hidden by the applied filter", diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index fe81e168d..808ad335f 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.EnsureThat; using NzbDrone.Common.Extensions; @@ -80,6 +79,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, IUpdateMediaInfo mediaInfoUpdater, @@ -241,20 +243,9 @@ namespace NzbDrone.Core.Organizer return "_"; } - public static string CleanFileName(string name, bool replace = true, ColonReplacementFormat colonReplacement = ColonReplacementFormat.Delete) + public static string CleanFileName(string name) { - var colonReplacementFormat = colonReplacement.GetFormatString(); - - var result = name; - string[] badCharacters = { "\\", "/", "<", ">", "?", "*", ":", "|", "\"" }; - string[] goodCharacters = { "+", "+", "", "", "!", "-", colonReplacementFormat, "", "" }; - - for (var 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) @@ -585,7 +576,7 @@ namespace NzbDrone.Core.Organizer replacementText = replacementText.Replace(" ", tokenMatch.Separator); } - replacementText = CleanFileName(replacementText, namingConfig.ReplaceIllegalCharacters, namingConfig.ColonReplacementFormat); + replacementText = CleanFileName(replacementText, namingConfig); if (!replacementText.IsNullOrWhiteSpace()) { @@ -657,6 +648,51 @@ namespace NzbDrone.Core.Organizer return ReservedDeviceNamesRegex.Replace(input, match => match.Value.Replace(".", "_")); } + private static string CleanFileName(string name, NamingConfig namingConfig) + { + var result = name; + + 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(' '); + } + private string Truncate(string input, string formatter) { if (input.IsNullOrWhiteSpace()) @@ -710,13 +746,12 @@ namespace NzbDrone.Core.Organizer } } - public enum MultiEpisodeStyle + public enum ColonReplacementFormat { - Extend = 0, - Duplicate = 1, - Repeat = 2, - Scene = 3, - Range = 4, - PrefixedRange = 5 + 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 77768253c..62698d401 100644 --- a/src/NzbDrone.Core/Organizer/NamingConfig.cs +++ b/src/NzbDrone.Core/Organizer/NamingConfig.cs @@ -8,8 +8,7 @@ namespace NzbDrone.Core.Organizer { RenameMovies = false, ReplaceIllegalCharacters = true, - ColonReplacementFormat = 0, - MultiEpisodeStyle = 0, + ColonReplacementFormat = ColonReplacementFormat.Smart, MovieFolderFormat = "{Movie Title} ({Release Year})", StandardMovieFormat = "{Movie Title} ({Release Year}) {Quality Full}", }; @@ -17,36 +16,7 @@ namespace NzbDrone.Core.Organizer public bool RenameMovies { get; set; } public bool ReplaceIllegalCharacters { get; set; } public ColonReplacementFormat ColonReplacementFormat { get; set; } - public int MultiEpisodeStyle { get; set; } public string StandardMovieFormat { get; set; } public string MovieFolderFormat { get; set; } } - - public enum ColonReplacementFormat - { - Delete = 0, - Dash = 1, - SpaceDash = 2, - SpaceDashSpace = 3 - } - - public static class ColonReplacementFormatMethods - { - public static string GetFormatString(this ColonReplacementFormat format) - { - switch (format) - { - case ColonReplacementFormat.Delete: - return ""; - case ColonReplacementFormat.Dash: - return "-"; - case ColonReplacementFormat.SpaceDash: - return " -"; - case ColonReplacementFormat.SpaceDashSpace: - return " - "; - default: - return ""; - } - } - } }