From 64382e13a4b4b1ef3e968e0aaa7c84f91d9302fd Mon Sep 17 00:00:00 2001 From: Qstick Date: Wed, 26 Feb 2020 22:11:53 -0500 Subject: [PATCH] New: Allow Nested Movie Folders --- .../MediaManagement/Naming/NamingModal.js | 1 + .../MovieTitleFirstCharacterFixture.cs | 57 ++++++++++++++++++ .../Events/MovieFolderCreatedEvent.cs | 1 + .../MediaFiles/MovieFileMovingService.cs | 58 ++++--------------- src/NzbDrone.Core/Movies/MoveMovieService.cs | 1 + src/NzbDrone.Core/Organizer/EpisodeFormat.cs | 8 --- .../Organizer/EpisodeSortingType.cs | 10 ---- .../Organizer/FileNameBuilder.cs | 47 ++++++++++++--- 8 files changed, 108 insertions(+), 75 deletions(-) create mode 100644 src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/MovieTitleFirstCharacterFixture.cs delete mode 100644 src/NzbDrone.Core/Organizer/EpisodeFormat.cs delete mode 100644 src/NzbDrone.Core/Organizer/EpisodeSortingType.cs diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingModal.js b/frontend/src/Settings/MediaManagement/Naming/NamingModal.js index b925c722b..229c12eef 100644 --- a/frontend/src/Settings/MediaManagement/Naming/NamingModal.js +++ b/frontend/src/Settings/MediaManagement/Naming/NamingModal.js @@ -117,6 +117,7 @@ class NamingModal extends Component { { token: '{Movie Title}', example: 'Movie Title!' }, { token: '{Movie CleanTitle}', example: 'Movie Title' }, { token: '{Movie TitleThe}', example: 'Movie Title, The' }, + { token: '{Movie TitleFirstCharacter}', example: 'M' }, { token: '{Movie Certification}', example: 'R' }, { token: '{Release Year}', example: '2009' } ]; diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/MovieTitleFirstCharacterFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/MovieTitleFirstCharacterFixture.cs new file mode 100644 index 000000000..f0e8f8731 --- /dev/null +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/MovieTitleFirstCharacterFixture.cs @@ -0,0 +1,57 @@ +using System.IO; +using System.Linq; +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +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 MovieTitleFirstCharacterFixture : CoreTest + { + private Movie _movie; + private NamingConfig _namingConfig; + + [SetUp] + public void Setup() + { + _movie = Builder + .CreateNew() + .Build(); + + _namingConfig = NamingConfig.Default; + _namingConfig.RenameEpisodes = 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", "M", "The Mist")] + [TestCase("A", "A", "A")] + [TestCase("30 Rock", "3", "30 Rock")] + public void should_get_expected_folder_name_back(string title, string parent, string child) + { + _movie.Title = title; + _namingConfig.MovieFolderFormat = "{Movie TitleFirstCharacter}\\{Movie Title}"; + + Subject.GetMovieFolder(_movie).Should().Be(Path.Combine(parent, child)); + } + + [Test] + public void should_be_able_to_use_lower_case_first_character() + { + _movie.Title = "Westworld"; + _namingConfig.MovieFolderFormat = "{movie titlefirstcharacter}\\{movie title}"; + + Subject.GetMovieFolder(_movie).Should().Be(Path.Combine("w", "westworld")); + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/Events/MovieFolderCreatedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/MovieFolderCreatedEvent.cs index b097145b8..ff50171ee 100644 --- a/src/NzbDrone.Core/MediaFiles/Events/MovieFolderCreatedEvent.cs +++ b/src/NzbDrone.Core/MediaFiles/Events/MovieFolderCreatedEvent.cs @@ -7,6 +7,7 @@ namespace NzbDrone.Core.MediaFiles.Events { public Movie Movie { get; private set; } public MovieFile MovieFile { get; private set; } + public string MovieFileFolder { get; set; } public string MovieFolder { get; set; } public MovieFolderCreatedEvent(Movie movie, MovieFile movieFile) diff --git a/src/NzbDrone.Core/MediaFiles/MovieFileMovingService.cs b/src/NzbDrone.Core/MediaFiles/MovieFileMovingService.cs index d22a324a9..a1af38c9e 100644 --- a/src/NzbDrone.Core/MediaFiles/MovieFileMovingService.cs +++ b/src/NzbDrone.Core/MediaFiles/MovieFileMovingService.cs @@ -1,6 +1,5 @@ using System; using System.IO; -using System.Linq; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.EnsureThat; @@ -24,35 +23,29 @@ namespace NzbDrone.Core.MediaFiles public class MovieFileMovingService : IMoveMovieFiles { - private readonly IMovieService _movieService; private readonly IUpdateMovieFileService _updateMovieFileService; private readonly IBuildFileNames _buildFileNames; private readonly IDiskTransferService _diskTransferService; private readonly IDiskProvider _diskProvider; private readonly IMediaFileAttributeService _mediaFileAttributeService; - private readonly IRecycleBinProvider _recycleBinProvider; private readonly IEventAggregator _eventAggregator; private readonly IConfigService _configService; private readonly Logger _logger; - public MovieFileMovingService(IMovieService movieService, - IUpdateMovieFileService updateMovieFileService, + public MovieFileMovingService(IUpdateMovieFileService updateMovieFileService, IBuildFileNames buildFileNames, IDiskTransferService diskTransferService, IDiskProvider diskProvider, IMediaFileAttributeService mediaFileAttributeService, - IRecycleBinProvider recycleBinProvider, IEventAggregator eventAggregator, IConfigService configService, Logger logger) { - _movieService = movieService; _updateMovieFileService = updateMovieFileService; _buildFileNames = buildFileNames; _diskTransferService = diskTransferService; _diskProvider = diskProvider; _mediaFileAttributeService = mediaFileAttributeService; - _recycleBinProvider = recycleBinProvider; _eventAggregator = eventAggregator; _configService = configService; _logger = logger; @@ -119,12 +112,6 @@ namespace NzbDrone.Core.MediaFiles _diskTransferService.TransferFile(movieFilePath, destinationFilePath, mode); - var oldMoviePath = movie.Path; - - var newMoviePath = new OsPath(destinationFilePath).Directory.FullPath.TrimEnd(Path.DirectorySeparatorChar); - - movie.Path = newMoviePath; //We update it when everything went well! - movieFile.RelativePath = movie.Path.GetRelativePath(destinationFilePath); _updateMovieFileService.ChangeFileDateForFile(movieFile, movie); @@ -140,37 +127,6 @@ namespace NzbDrone.Core.MediaFiles _mediaFileAttributeService.SetFilePermissions(destinationFilePath); - if (oldMoviePath != newMoviePath && _diskProvider.FolderExists(oldMoviePath)) - { - //Let's move the old files before deleting the old folder. We could just do move folder, but the main file (movie file) is already moved, so eh. - var files = _diskProvider.GetFiles(oldMoviePath, SearchOption.AllDirectories); - - foreach (var file in files) - { - try - { - var destFile = Path.Combine(newMoviePath, oldMoviePath.GetRelativePath(file)); - _diskProvider.EnsureFolder(Path.GetDirectoryName(destFile)); - _diskProvider.MoveFile(file, destFile); - } - catch (Exception e) - { - _logger.Warn(e, "Error while trying to move extra file {0} to new folder. Maybe it already exists? (Manual cleanup necessary!).", oldMoviePath.GetRelativePath(file)); - } - } - - if (_diskProvider.GetFiles(oldMoviePath, SearchOption.AllDirectories).Count() == 0) - { - _recycleBinProvider.DeleteFolder(oldMoviePath); - } - } - - //Only update the movie path if we were successfull! - if (oldMoviePath != newMoviePath) - { - _movieService.UpdateMovie(movie); - } - return movieFile; } @@ -181,11 +137,10 @@ namespace NzbDrone.Core.MediaFiles private void EnsureMovieFolder(MovieFile movieFile, Movie movie, string filePath) { - var movieFolder = Path.GetDirectoryName(filePath); + var movieFileFolder = Path.GetDirectoryName(filePath); - //movie.Path = movieFolder; + var movieFolder = movie.Path; var rootFolder = new OsPath(movieFolder).Directory.FullPath; - var fileName = Path.GetFileName(filePath); if (!_diskProvider.FolderExists(rootFolder)) { @@ -202,6 +157,13 @@ namespace NzbDrone.Core.MediaFiles changed = true; } + if (movieFolder != movieFileFolder && !_diskProvider.FolderExists(movieFileFolder)) + { + CreateFolder(movieFileFolder); + newEvent.MovieFileFolder = movieFileFolder; + changed = true; + } + if (changed) { _eventAggregator.PublishEvent(newEvent); diff --git a/src/NzbDrone.Core/Movies/MoveMovieService.cs b/src/NzbDrone.Core/Movies/MoveMovieService.cs index 071b04d00..9e1fbb03e 100644 --- a/src/NzbDrone.Core/Movies/MoveMovieService.cs +++ b/src/NzbDrone.Core/Movies/MoveMovieService.cs @@ -53,6 +53,7 @@ namespace NzbDrone.Core.Movies try { + _diskProvider.CreateFolder(new DirectoryInfo(destinationPath).Parent.FullName); _diskTransferService.TransferFolder(sourcePath, destinationPath, TransferMode.Move); _logger.ProgressInfo("{0} moved successfully to {1}", movie.Title, movie.Path); diff --git a/src/NzbDrone.Core/Organizer/EpisodeFormat.cs b/src/NzbDrone.Core/Organizer/EpisodeFormat.cs deleted file mode 100644 index c23dc85aa..000000000 --- a/src/NzbDrone.Core/Organizer/EpisodeFormat.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace NzbDrone.Core.Organizer -{ - public class AbsoluteEpisodeFormat - { - public string Separator { get; set; } - public string AbsoluteEpisodePattern { get; set; } - } -} diff --git a/src/NzbDrone.Core/Organizer/EpisodeSortingType.cs b/src/NzbDrone.Core/Organizer/EpisodeSortingType.cs deleted file mode 100644 index 77da1145d..000000000 --- a/src/NzbDrone.Core/Organizer/EpisodeSortingType.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace NzbDrone.Core.Organizer -{ - public class EpisodeSortingType - { - public int Id { get; set; } - public string Name { get; set; } - public string Pattern { get; set; } - public string EpisodeSeparator { get; set; } - } -} diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 5b39f09fa..faa9294cd 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -105,11 +105,25 @@ namespace NzbDrone.Core.Organizer AddTagsTokens(tokenHandlers, movieFile); AddCustomFormats(tokenHandlers, movie, movieFile, customFormats); - var fileName = ReplaceTokens(pattern, tokenHandlers, namingConfig).Trim(); - fileName = FileNameCleanupRegex.Replace(fileName, match => match.Captures[0].Value[0].ToString()); - fileName = TrimSeparatorsRegex.Replace(fileName, string.Empty); + var splitPatterns = pattern.Split(new char[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries); + var components = new List(); - return fileName; + foreach (var s in splitPatterns) + { + var splitPattern = s; + + var component = ReplaceTokens(splitPattern, tokenHandlers, namingConfig).Trim(); + + component = FileNameCleanupRegex.Replace(component, match => match.Captures[0].Value[0].ToString()); + component = TrimSeparatorsRegex.Replace(component, string.Empty); + + if (component.IsNotNullOrWhiteSpace()) + { + components.Add(component); + } + } + + return Path.Combine(components.ToArray()); } public string BuildFilePath(Movie movie, string fileName, string extension) @@ -154,8 +168,23 @@ namespace NzbDrone.Core.Organizer AddMovieFileTokens(tokenHandlers, new MovieFile { SceneName = $"{movie.Title} {movie.Year}", RelativePath = $"{movie.Title} {movie.Year}" }); } - string name = ReplaceTokens(namingConfig.MovieFolderFormat, tokenHandlers, namingConfig); - return CleanFolderName(name, namingConfig.ReplaceIllegalCharacters, namingConfig.ColonReplacementFormat); + 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) @@ -188,12 +217,11 @@ namespace NzbDrone.Core.Organizer return result.Trim(); } - public static string CleanFolderName(string name, bool replace = true, ColonReplacementFormat colonReplacement = ColonReplacementFormat.Delete) + public static string CleanFolderName(string name) { name = FileNameCleanupRegex.Replace(name, match => match.Captures[0].Value[0].ToString()); - name = name.Trim(' ', '.'); - return CleanFileName(name, replace, colonReplacement); + return name.Trim(' ', '.'); } private void AddMovieTokens(Dictionary> tokenHandlers, Movie movie) @@ -202,6 +230,7 @@ namespace NzbDrone.Core.Organizer tokenHandlers["{Movie CleanTitle}"] = m => CleanTitle(movie.Title); tokenHandlers["{Movie Title The}"] = m => TitleThe(movie.Title); tokenHandlers["{Movie Certification}"] = mbox => movie.Certification; + tokenHandlers["{Movie TitleFirstCharacter}"] = m => TitleThe(movie.Title).Substring(0, 1).FirstCharToUpper(); } private void AddTagsTokens(Dictionary> tokenHandlers, MovieFile movieFile)