diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeFileMovingServiceTests/MoveEpisodeFileFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeFileMovingServiceTests/MoveEpisodeFileFixture.cs index 73b6978a5..10890d5c7 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeFileMovingServiceTests/MoveEpisodeFileFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeFileMovingServiceTests/MoveEpisodeFileFixture.cs @@ -5,11 +5,15 @@ using Moq; using NUnit.Framework; using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Organizer; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; +using NzbDrone.Test.Common; namespace NzbDrone.Core.Test.MediaFiles.EpisodeFileMovingServiceTests { @@ -24,7 +28,7 @@ public class MoveEpisodeFileFixture : CoreTest public void Setup() { _series = Builder.CreateNew() - .With(s => s.Path = @"C:\Test\TV\Series") + .With(s => s.Path = @"C:\Test\TV\Series".AsOsAgnostic()) .Build(); _episodeFile = Builder.CreateNew() @@ -43,7 +47,16 @@ public void Setup() Mocker.GetMock() .Setup(s => s.BuildFilePath(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(@"C:\Test\TV\Series\File Name.avi"); + .Returns(@"C:\Test\TV\Series\Season 01\File Name.avi".AsOsAgnostic()); + + Mocker.GetMock() + .Setup(s => s.BuildSeasonPath(It.IsAny(), It.IsAny())) + .Returns(@"C:\Test\TV\Series\Season 01".AsOsAgnostic()); + + var rootFolder = @"C:\Test\TV".AsOsAgnostic(); + Mocker.GetMock() + .Setup(s => s.FolderExists(rootFolder)) + .Returns(true); Mocker.GetMock() .Setup(s => s.FileExists(It.IsAny())) @@ -73,5 +86,39 @@ public void should_catch_InvalidOperationException_during_folder_inheritance() Subject.MoveEpisodeFile(_episodeFile, _localEpisode); } + + [Test] + public void should_notify_on_series_folder_creation() + { + Subject.MoveEpisodeFile(_episodeFile, _localEpisode); + + Mocker.GetMock() + .Verify(s => s.PublishEvent(It.Is(p => + p.SeriesFolder.IsNotNullOrWhiteSpace())), Times.Once()); + } + + [Test] + public void should_notify_on_season_folder_creation() + { + Subject.MoveEpisodeFile(_episodeFile, _localEpisode); + + Mocker.GetMock() + .Verify(s => s.PublishEvent(It.Is(p => + p.SeasonFolder.IsNotNullOrWhiteSpace())), Times.Once()); + } + + [Test] + public void should_not_notify_if_series_folder_already_exists() + { + Mocker.GetMock() + .Setup(s => s.FolderExists(_series.Path)) + .Returns(true); + + Subject.MoveEpisodeFile(_episodeFile, _localEpisode); + + Mocker.GetMock() + .Verify(s => s.PublishEvent(It.Is(p => + p.SeriesFolder.IsNotNullOrWhiteSpace())), Times.Never()); + } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs index dffcf5bfa..d7760a7a9 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs @@ -7,6 +7,8 @@ using NzbDrone.Common.EnsureThat; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Organizer; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Tv; @@ -27,6 +29,7 @@ public class EpisodeFileMovingService : IMoveEpisodeFiles private readonly IBuildFileNames _buildFileNames; private readonly IDiskProvider _diskProvider; private readonly IMediaFileAttributeService _mediaFileAttributeService; + private readonly IEventAggregator _eventAggregator; private readonly IConfigService _configService; private readonly Logger _logger; @@ -35,6 +38,7 @@ public EpisodeFileMovingService(IEpisodeService episodeService, IBuildFileNames buildFileNames, IDiskProvider diskProvider, IMediaFileAttributeService mediaFileAttributeService, + IEventAggregator eventAggregator, IConfigService configService, Logger logger) { @@ -43,6 +47,7 @@ public EpisodeFileMovingService(IEpisodeService episodeService, _buildFileNames = buildFileNames; _diskProvider = diskProvider; _mediaFileAttributeService = mediaFileAttributeService; + _eventAggregator = eventAggregator; _configService = configService; _logger = logger; } @@ -53,6 +58,8 @@ public EpisodeFile MoveEpisodeFile(EpisodeFile episodeFile, Series series) var newFileName = _buildFileNames.BuildFileName(episodes, series, episodeFile); var filePath = _buildFileNames.BuildFilePath(series, episodes.First().SeasonNumber, newFileName, Path.GetExtension(episodeFile.RelativePath)); + EnsureEpisodeFolder(episodeFile, series, episodes.Select(v => v.SeasonNumber).First(), filePath); + _logger.Debug("Renaming episode file: {0} to {1}", episodeFile, filePath); return TransferFile(episodeFile, series, episodes, filePath, TransferMode.Move); @@ -63,6 +70,8 @@ public EpisodeFile MoveEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEp var newFileName = _buildFileNames.BuildFileName(localEpisode.Episodes, localEpisode.Series, episodeFile); var filePath = _buildFileNames.BuildFilePath(localEpisode.Series, localEpisode.SeasonNumber, newFileName, Path.GetExtension(localEpisode.Path)); + EnsureEpisodeFolder(episodeFile, localEpisode, filePath); + _logger.Debug("Moving episode file: {0} to {1}", episodeFile.Path, filePath); return TransferFile(episodeFile, localEpisode.Series, localEpisode.Episodes, filePath, TransferMode.Move); @@ -73,6 +82,8 @@ public EpisodeFile CopyEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEp var newFileName = _buildFileNames.BuildFileName(localEpisode.Episodes, localEpisode.Series, episodeFile); var filePath = _buildFileNames.BuildFilePath(localEpisode.Series, localEpisode.SeasonNumber, newFileName, Path.GetExtension(localEpisode.Path)); + EnsureEpisodeFolder(episodeFile, localEpisode, filePath); + if (_configService.CopyUsingHardlinks) { _logger.Debug("Hardlinking episode file: {0} to {1}", episodeFile.Path, filePath); @@ -101,27 +112,6 @@ private EpisodeFile TransferFile(EpisodeFile episodeFile, Series series, List [{2}]", mode, episodeFilePath, destinationFilename); _diskProvider.TransferFile(episodeFilePath, destinationFilename, mode); @@ -150,5 +140,74 @@ private EpisodeFile TransferFile(EpisodeFile episodeFile, Series series, List directoryName).IsNotNullOrWhiteSpace(); + + var parentFolder = Path.GetDirectoryName(directoryName); + if (!_diskProvider.FolderExists(parentFolder)) + { + CreateFolder(parentFolder); + } + + try + { + _diskProvider.CreateFolder(directoryName); + } + catch (IOException ex) + { + _logger.ErrorException("Unable to create directory: " + directoryName, ex); + } + + _mediaFileAttributeService.SetFolderPermissions(directoryName); + } } } diff --git a/src/NzbDrone.Core/MediaFiles/Events/EpisodeFolderCreatedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/EpisodeFolderCreatedEvent.cs new file mode 100644 index 000000000..126b21222 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/Events/EpisodeFolderCreatedEvent.cs @@ -0,0 +1,20 @@ +using NzbDrone.Common.Messaging; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.MediaFiles.Events +{ + public class EpisodeFolderCreatedEvent : IEvent + { + public Series Series { get; private set; } + public EpisodeFile EpisodeFile { get; private set; } + public string SeriesFolder { get; set; } + public string SeasonFolder { get; set; } + public string EpisodeFolder { get; set; } + + public EpisodeFolderCreatedEvent(Series series, EpisodeFile episodeFile) + { + Series = series; + EpisodeFile = episodeFile; + } + } +} diff --git a/src/NzbDrone.Core/Metadata/MetadataService.cs b/src/NzbDrone.Core/Metadata/MetadataService.cs index 9ead5e9b8..cf1e1f325 100644 --- a/src/NzbDrone.Core/Metadata/MetadataService.cs +++ b/src/NzbDrone.Core/Metadata/MetadataService.cs @@ -19,6 +19,7 @@ namespace NzbDrone.Core.Metadata { public class MetadataService : IHandle, IHandle, + IHandle, IHandle { private readonly IMetadataFactory _metadataFactory; @@ -100,6 +101,35 @@ public void Handle(EpisodeImportedEvent message) } } + public void Handle(EpisodeFolderCreatedEvent message) + { + if (message.SeriesFolder.IsNullOrWhiteSpace() && message.SeasonFolder.IsNullOrWhiteSpace()) + { + return; + } + + var seriesMetadataFiles = _metadataFileService.GetFilesBySeries(message.Series.Id); + + foreach (var consumer in _metadataFactory.Enabled()) + { + var files = new List(); + var consumerFiles = GetMetadataFilesForConsumer(consumer, seriesMetadataFiles); + + if (message.SeriesFolder.IsNotNullOrWhiteSpace()) + { + files.AddIfNotNull(ProcessSeriesMetadata(consumer, message.Series, consumerFiles)); + files.AddRange(ProcessSeriesImages(consumer, message.Series, consumerFiles)); + } + + if (message.SeasonFolder.IsNotNullOrWhiteSpace()) + { + files.AddRange(ProcessSeasonImages(consumer, message.Series, consumerFiles)); + } + + _eventAggregator.PublishEvent(new MetadataFilesUpdated(files)); + } + } + public void Handle(SeriesRenamedEvent message) { var seriesMetadata = _metadataFileService.GetFilesBySeries(message.Series.Id); diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index c953ec0b0..eab237394 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -608,6 +608,7 @@ + diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index faeda4099..125a849d1 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -17,6 +17,7 @@ public interface IBuildFileNames { string BuildFileName(List episodes, Series series, EpisodeFile episodeFile, NamingConfig namingConfig = null); string BuildFilePath(Series series, Int32 seasonNumber, String fileName, String extension); + string BuildSeasonPath(Series series, Int32 seasonNumber); BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec); string GetSeriesFolder(Series series, NamingConfig namingConfig = null); string GetSeasonFolder(Series series, Int32 seasonNumber, NamingConfig namingConfig = null); @@ -138,29 +139,32 @@ public string BuildFilePath(Series series, int seasonNumber, string fileName, st { Ensure.That(extension, () => extension).IsNotNullOrWhiteSpace(); - string path = series.Path; + var path = BuildSeasonPath(series, seasonNumber); + + return Path.Combine(path, fileName + extension); + } + + public string BuildSeasonPath(Series series, int seasonNumber) + { + var path = series.Path; if (series.SeasonFolder) { - string seasonFolder; - if (seasonNumber == 0) { - seasonFolder = "Specials"; + path = Path.Combine(path, "Specials"); } - else { - var nameSpec = _namingConfigService.GetConfig(); - seasonFolder = GetSeasonFolder(series, seasonNumber, nameSpec); + var seasonFolder = GetSeasonFolder(series, seasonNumber); + + seasonFolder = CleanFileName(seasonFolder); + + path = Path.Combine(path, seasonFolder); } - - seasonFolder = CleanFileName(seasonFolder); - - path = Path.Combine(path, seasonFolder); } - return Path.Combine(path, fileName + extension); + return path; } public BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec)