1
0
mirror of https://github.com/Radarr/Radarr.git synced 2024-09-17 15:02:34 +02:00

New: Allow Nested Movie Folders

This commit is contained in:
Qstick 2020-02-26 22:11:53 -05:00
parent 232682f109
commit 64382e13a4
8 changed files with 108 additions and 75 deletions

View File

@ -117,6 +117,7 @@ class NamingModal extends Component {
{ token: '{Movie Title}', example: 'Movie Title!' }, { token: '{Movie Title}', example: 'Movie Title!' },
{ token: '{Movie CleanTitle}', example: 'Movie Title' }, { token: '{Movie CleanTitle}', example: 'Movie Title' },
{ token: '{Movie TitleThe}', example: 'Movie Title, The' }, { token: '{Movie TitleThe}', example: 'Movie Title, The' },
{ token: '{Movie TitleFirstCharacter}', example: 'M' },
{ token: '{Movie Certification}', example: 'R' }, { token: '{Movie Certification}', example: 'R' },
{ token: '{Release Year}', example: '2009' } { token: '{Release Year}', example: '2009' }
]; ];

View File

@ -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<FileNameBuilder>
{
private Movie _movie;
private NamingConfig _namingConfig;
[SetUp]
public void Setup()
{
_movie = Builder<Movie>
.CreateNew()
.Build();
_namingConfig = NamingConfig.Default;
_namingConfig.RenameEpisodes = true;
Mocker.GetMock<INamingConfigService>()
.Setup(c => c.GetConfig()).Returns(_namingConfig);
Mocker.GetMock<IQualityDefinitionService>()
.Setup(v => v.Get(Moq.It.IsAny<Quality>()))
.Returns<Quality>(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"));
}
}
}

View File

@ -7,6 +7,7 @@ public class MovieFolderCreatedEvent : IEvent
{ {
public Movie Movie { get; private set; } public Movie Movie { get; private set; }
public MovieFile MovieFile { get; private set; } public MovieFile MovieFile { get; private set; }
public string MovieFileFolder { get; set; }
public string MovieFolder { get; set; } public string MovieFolder { get; set; }
public MovieFolderCreatedEvent(Movie movie, MovieFile movieFile) public MovieFolderCreatedEvent(Movie movie, MovieFile movieFile)

View File

@ -1,6 +1,5 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq;
using NLog; using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.EnsureThat; using NzbDrone.Common.EnsureThat;
@ -24,35 +23,29 @@ public interface IMoveMovieFiles
public class MovieFileMovingService : IMoveMovieFiles public class MovieFileMovingService : IMoveMovieFiles
{ {
private readonly IMovieService _movieService;
private readonly IUpdateMovieFileService _updateMovieFileService; private readonly IUpdateMovieFileService _updateMovieFileService;
private readonly IBuildFileNames _buildFileNames; private readonly IBuildFileNames _buildFileNames;
private readonly IDiskTransferService _diskTransferService; private readonly IDiskTransferService _diskTransferService;
private readonly IDiskProvider _diskProvider; private readonly IDiskProvider _diskProvider;
private readonly IMediaFileAttributeService _mediaFileAttributeService; private readonly IMediaFileAttributeService _mediaFileAttributeService;
private readonly IRecycleBinProvider _recycleBinProvider;
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
private readonly IConfigService _configService; private readonly IConfigService _configService;
private readonly Logger _logger; private readonly Logger _logger;
public MovieFileMovingService(IMovieService movieService, public MovieFileMovingService(IUpdateMovieFileService updateMovieFileService,
IUpdateMovieFileService updateMovieFileService,
IBuildFileNames buildFileNames, IBuildFileNames buildFileNames,
IDiskTransferService diskTransferService, IDiskTransferService diskTransferService,
IDiskProvider diskProvider, IDiskProvider diskProvider,
IMediaFileAttributeService mediaFileAttributeService, IMediaFileAttributeService mediaFileAttributeService,
IRecycleBinProvider recycleBinProvider,
IEventAggregator eventAggregator, IEventAggregator eventAggregator,
IConfigService configService, IConfigService configService,
Logger logger) Logger logger)
{ {
_movieService = movieService;
_updateMovieFileService = updateMovieFileService; _updateMovieFileService = updateMovieFileService;
_buildFileNames = buildFileNames; _buildFileNames = buildFileNames;
_diskTransferService = diskTransferService; _diskTransferService = diskTransferService;
_diskProvider = diskProvider; _diskProvider = diskProvider;
_mediaFileAttributeService = mediaFileAttributeService; _mediaFileAttributeService = mediaFileAttributeService;
_recycleBinProvider = recycleBinProvider;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
_configService = configService; _configService = configService;
_logger = logger; _logger = logger;
@ -119,12 +112,6 @@ private MovieFile TransferFile(MovieFile movieFile, Movie movie, string destinat
_diskTransferService.TransferFile(movieFilePath, destinationFilePath, mode); _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); movieFile.RelativePath = movie.Path.GetRelativePath(destinationFilePath);
_updateMovieFileService.ChangeFileDateForFile(movieFile, movie); _updateMovieFileService.ChangeFileDateForFile(movieFile, movie);
@ -140,37 +127,6 @@ private MovieFile TransferFile(MovieFile movieFile, Movie movie, string destinat
_mediaFileAttributeService.SetFilePermissions(destinationFilePath); _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; return movieFile;
} }
@ -181,11 +137,10 @@ private void EnsureMovieFolder(MovieFile movieFile, LocalMovie localMovie, strin
private void EnsureMovieFolder(MovieFile movieFile, Movie movie, string filePath) 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 rootFolder = new OsPath(movieFolder).Directory.FullPath;
var fileName = Path.GetFileName(filePath);
if (!_diskProvider.FolderExists(rootFolder)) if (!_diskProvider.FolderExists(rootFolder))
{ {
@ -202,6 +157,13 @@ private void EnsureMovieFolder(MovieFile movieFile, Movie movie, string filePath
changed = true; changed = true;
} }
if (movieFolder != movieFileFolder && !_diskProvider.FolderExists(movieFileFolder))
{
CreateFolder(movieFileFolder);
newEvent.MovieFileFolder = movieFileFolder;
changed = true;
}
if (changed) if (changed)
{ {
_eventAggregator.PublishEvent(newEvent); _eventAggregator.PublishEvent(newEvent);

View File

@ -53,6 +53,7 @@ private void MoveSingleMovie(Movie movie, string sourcePath, string destinationP
try try
{ {
_diskProvider.CreateFolder(new DirectoryInfo(destinationPath).Parent.FullName);
_diskTransferService.TransferFolder(sourcePath, destinationPath, TransferMode.Move); _diskTransferService.TransferFolder(sourcePath, destinationPath, TransferMode.Move);
_logger.ProgressInfo("{0} moved successfully to {1}", movie.Title, movie.Path); _logger.ProgressInfo("{0} moved successfully to {1}", movie.Title, movie.Path);

View File

@ -1,8 +0,0 @@
namespace NzbDrone.Core.Organizer
{
public class AbsoluteEpisodeFormat
{
public string Separator { get; set; }
public string AbsoluteEpisodePattern { get; set; }
}
}

View File

@ -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; }
}
}

View File

@ -105,11 +105,25 @@ public string BuildFileName(Movie movie, MovieFile movieFile, NamingConfig namin
AddTagsTokens(tokenHandlers, movieFile); AddTagsTokens(tokenHandlers, movieFile);
AddCustomFormats(tokenHandlers, movie, movieFile, customFormats); AddCustomFormats(tokenHandlers, movie, movieFile, customFormats);
var fileName = ReplaceTokens(pattern, tokenHandlers, namingConfig).Trim(); var splitPatterns = pattern.Split(new char[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries);
fileName = FileNameCleanupRegex.Replace(fileName, match => match.Captures[0].Value[0].ToString()); var components = new List<string>();
fileName = TrimSeparatorsRegex.Replace(fileName, string.Empty);
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) public string BuildFilePath(Movie movie, string fileName, string extension)
@ -154,8 +168,23 @@ public string GetMovieFolder(Movie movie, NamingConfig namingConfig = null)
AddMovieFileTokens(tokenHandlers, new MovieFile { SceneName = $"{movie.Title} {movie.Year}", RelativePath = $"{movie.Title} {movie.Year}" }); AddMovieFileTokens(tokenHandlers, new MovieFile { SceneName = $"{movie.Title} {movie.Year}", RelativePath = $"{movie.Title} {movie.Year}" });
} }
string name = ReplaceTokens(namingConfig.MovieFolderFormat, tokenHandlers, namingConfig); var splitPatterns = pattern.Split(new char[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries);
return CleanFolderName(name, namingConfig.ReplaceIllegalCharacters, namingConfig.ColonReplacementFormat); var components = new List<string>();
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) public static string CleanTitle(string title)
@ -188,12 +217,11 @@ public static string CleanFileName(string name, bool replace = true, ColonReplac
return result.Trim(); 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 = 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<string, Func<TokenMatch, string>> tokenHandlers, Movie movie) private void AddMovieTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, Movie movie)
@ -202,6 +230,7 @@ private void AddMovieTokens(Dictionary<string, Func<TokenMatch, string>> tokenHa
tokenHandlers["{Movie CleanTitle}"] = m => CleanTitle(movie.Title); tokenHandlers["{Movie CleanTitle}"] = m => CleanTitle(movie.Title);
tokenHandlers["{Movie Title The}"] = m => TitleThe(movie.Title); tokenHandlers["{Movie Title The}"] = m => TitleThe(movie.Title);
tokenHandlers["{Movie Certification}"] = mbox => movie.Certification; tokenHandlers["{Movie Certification}"] = mbox => movie.Certification;
tokenHandlers["{Movie TitleFirstCharacter}"] = m => TitleThe(movie.Title).Substring(0, 1).FirstCharToUpper();
} }
private void AddTagsTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, MovieFile movieFile) private void AddTagsTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, MovieFile movieFile)