diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/ArtistNameFirstCharacterGroupFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/ArtistNameFirstCharacterGroupFixture.cs new file mode 100644 index 000000000..fbbc7783f --- /dev/null +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/ArtistNameFirstCharacterGroupFixture.cs @@ -0,0 +1,64 @@ +using System.IO; +using System.Linq; +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +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 ArtistNameFirstCharacterGroupFixture : CoreTest + { + private Artist _artist; + private NamingConfig _namingConfig; + + [SetUp] + public void Setup() + { + _artist = Builder + .CreateNew() + .Build(); + + _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)); + } + + [TestCase("The Mist", "MNO", "The Mist")] + [TestCase("A", "ABC", "A")] + [TestCase("30 Rock", "0-9", "30 Rock")] + [TestCase("The '80s Greatest", "0-9", "The '80s Greatest")] + [TestCase("좀비버스", "좀", "좀비버스")] + [TestCase("¡Mucha Lucha!", "MNO", "¡Mucha Lucha!")] + [TestCase(".hack", "GHI", "hack")] + [TestCase("Ütopya", "STU", "Ütopya")] + [TestCase("Æon Flux", "ABC", "Æon Flux")] + [TestCase("Yabbadabbadoo", "YZ", "Yabbadabbadoo")] + public void should_get_expected_folder_name_back(string title, string parent, string child) + { + _artist.Name = title; + _namingConfig.ArtistFolderFormat = "{Artist NameFirstCharacterGroup}\\{Artist Name}"; + + Subject.GetArtistFolder(_artist).Should().Be(Path.Combine(parent, child)); + } + + [Test] + public void should_be_able_to_use_lower_case_first_character() + { + _artist.Name = "Westworld"; + _namingConfig.ArtistFolderFormat = "{Artist NameFirstCharacterGroup}\\{artist name}"; + + Subject.GetArtistFolder(_artist).Should().Be(Path.Combine("VWX", "westworld")); + } + } +} diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 36ec1227e..4756dfad8 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -72,6 +72,13 @@ namespace NzbDrone.Core.Organizer private static readonly Regex ReservedDeviceNamesRegex = new Regex(@"^(?:aux|com[1-9]|con|lpt[1-9]|nul|prn)\.", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly List<(string Range, string Group)> ArtistNameGroups = new List<(string Range, string Group)> + { + ("0-9", "0-9"), ("A-C", "ABC"), ("D-F", "DEF"), ("G-I", "GHI"), + ("J-L", "JKL"), ("M-O", "MNO"), ("P-R", "PQR"), ("S-U", "STU"), + ("V-X", "VWX"), ("Y-Z", "YZ") + }; + public FileNameBuilder(INamingConfigService namingConfigService, IQualityDefinitionService qualityDefinitionService, ICacheManager cacheManager, @@ -282,6 +289,24 @@ namespace NzbDrone.Core.Organizer return "_"; } + public static string TitleFirstCharacterGroup(string title) + { + var firstChar = TitleFirstCharacter(title); + if (firstChar == "_") + { + return firstChar; + } + + var group = ArtistNameGroups.FirstOrDefault(g => firstChar[0] >= g.Range[0] && firstChar[0] <= g.Range[2]); + if (group == default) + { + // If it turns out not to be a Latin character, fall back to the single character + return firstChar; + } + + return group.Group; + } + public static string CleanFileName(string name) { return CleanFileName(name, NamingConfig.Default); @@ -301,6 +326,7 @@ namespace NzbDrone.Core.Organizer tokenHandlers["{Artist NameThe}"] = m => TitleThe(artist.Name); tokenHandlers["{Artist Genre}"] = m => artist.Metadata.Value.Genres?.FirstOrDefault() ?? string.Empty; tokenHandlers["{Artist NameFirstCharacter}"] = m => TitleFirstCharacter(TitleThe(artist.Name)); + tokenHandlers["{Artist NameFirstCharacterGroup"] = m => TitleFirstCharacterGroup(TitleThe(artist.Name)); tokenHandlers["{Artist MbId}"] = m => artist.ForeignArtistId ?? string.Empty; if (artist.Metadata.Value.Disambiguation != null)