diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/NestedFileNameBuilderFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/NestedFileNameBuilderFixture.cs new file mode 100644 index 000000000..16da0dea0 --- /dev/null +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/NestedFileNameBuilderFixture.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Books; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Organizer; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests +{ + [TestFixture] + public class NestedFileNameBuilderFixture : CoreTest + { + private Author _artist; + private Book _album; + private Edition _release; + private BookFile _trackFile; + private NamingConfig _namingConfig; + + [SetUp] + public void Setup() + { + _artist = Builder + .CreateNew() + .With(s => s.Name = "AuthorName") + .With(s => s.Metadata = new AuthorMetadata + { + Disambiguation = "US Author", + Name = "AuthorName" + }) + .Build(); + + _album = Builder + .CreateNew() + .With(s => s.Author = _artist) + .With(s => s.AuthorMetadata = _artist.Metadata.Value) + .With(s => s.Title = "A Novel") + .With(s => s.ReleaseDate = new DateTime(2020, 1, 15)) + .With(s => s.SeriesLinks = new List()) + .Build(); + + _release = Builder + .CreateNew() + .With(s => s.Monitored = true) + .With(s => s.Book = _album) + .With(s => s.Title = "A Novel") + .With(s => s.ReleaseDate = new DateTime(2020, 1, 15)) + .Build(); + + _namingConfig = NamingConfig.Default; + _namingConfig.RenameBooks = true; + + Mocker.GetMock() + .Setup(c => c.GetConfig()).Returns(_namingConfig); + + _trackFile = Builder.CreateNew() + .With(e => e.Quality = new QualityModel(Quality.MOBI)) + .With(e => e.ReleaseGroup = "ReadarrTest") + .Build(); + + Mocker.GetMock() + .Setup(v => v.Get(Moq.It.IsAny())) + .Returns(v => Quality.DefaultQualityDefinitions.First(c => c.Quality == v)); + } + + private void WithSeries() + { + _album.SeriesLinks = new List + { + new SeriesBookLink + { + Series = new Series + { + Title = "A Series", + }, + Position = "2-3", + SeriesPosition = 1 + } + }; + } + + [Test] + public void should_build_nested_standard_track_filename_with_forward_slash() + { + WithSeries(); + + _namingConfig.StandardBookFormat = "{Book Series}/{Book SeriesTitle - }{Book Title} {(Release Year)}"; + + var name = Subject.BuildBookFileName(_artist, _release, _trackFile) + .Should().Be("A Series\\A Series #2-3 - A Novel (2020)".AsOsAgnostic()); + } + + [Test] + public void should_build_standard_track_filename_with_forward_slash() + { + _namingConfig.StandardBookFormat = "{Book Series}/{Book SeriesTitle - }{Book Title} {(Release Year)}"; + + Subject.BuildBookFileName(_artist, _release, _trackFile) + .Should().Be("A Novel (2020)".AsOsAgnostic()); + } + + [Test] + public void should_build_nested_standard_track_filename_with_back_slash() + { + WithSeries(); + + _namingConfig.StandardBookFormat = "{Book Series}\\{Book SeriesTitle - }{Book Title} {(Release Year)}"; + + Subject.BuildBookFileName(_artist, _release, _trackFile) + .Should().Be("A Series\\A Series #2-3 - A Novel (2020)".AsOsAgnostic()); + } + + [Test] + public void should_build_standard_track_filename_with_back_slash() + { + _namingConfig.StandardBookFormat = "{Book Series}\\{Book SeriesTitle - }{Book Title} {(Release Year)}"; + + Subject.BuildBookFileName(_artist, _release, _trackFile) + .Should().Be("A Novel (2020)".AsOsAgnostic()); + } + } +} diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 1d59b0e74..51bf9be49 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -88,9 +88,6 @@ namespace NzbDrone.Core.Organizer var pattern = namingConfig.StandardBookFormat; - var subFolders = pattern.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries); - var safePattern = subFolders.Aggregate("", (current, folderLevel) => Path.Combine(current, folderLevel)); - var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); AddAuthorTokens(tokenHandlers, author); @@ -100,13 +97,26 @@ namespace NzbDrone.Core.Organizer AddMediaInfoTokens(tokenHandlers, bookFile); AddPreferredWords(tokenHandlers, author, bookFile, preferredWords); - var fileName = ReplacePartTokens(safePattern, tokenHandlers, namingConfig); - fileName = ReplaceTokens(fileName, tokenHandlers, namingConfig).Trim(); + var splitPatterns = pattern.Split(new char[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries); + var components = new List(); + + foreach (var s in splitPatterns) + { + var splitPattern = s; + + var component = ReplacePartTokens(splitPattern, tokenHandlers, namingConfig).Trim(); + component = ReplaceTokens(component, tokenHandlers, namingConfig).Trim(); - fileName = FileNameCleanupRegex.Replace(fileName, match => match.Captures[0].Value[0].ToString()); - fileName = TrimSeparatorsRegex.Replace(fileName, string.Empty); + component = FileNameCleanupRegex.Replace(component, match => match.Captures[0].Value[0].ToString()); + component = TrimSeparatorsRegex.Replace(component, string.Empty); + + if (component.IsNotNullOrWhiteSpace()) + { + components.Add(component); + } + } - return fileName; + return Path.Combine(components.ToArray()); } public string BuildBookFilePath(Author author, Edition edition, string fileName, string extension) @@ -175,11 +185,28 @@ namespace NzbDrone.Core.Organizer namingConfig = _namingConfigService.GetConfig(); } + var pattern = namingConfig.AuthorFolderFormat; var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); AddAuthorTokens(tokenHandlers, author); - return CleanFolderName(ReplaceTokens(namingConfig.AuthorFolderFormat, tokenHandlers, namingConfig)); + var splitPatterns = pattern.Split(new char[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries); + var components = new List(); + + foreach (var s in splitPatterns) + { + var splitPattern = s; + + var component = ReplaceTokens(splitPattern, tokenHandlers, namingConfig); + component = CleanFolderName(component); + + if (component.IsNotNullOrWhiteSpace()) + { + components.Add(component); + } + } + + return Path.Combine(components.ToArray()); } public static string CleanTitle(string title)