diff --git a/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs index 8f33db4f0..a0eecb805 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs @@ -12,6 +12,7 @@ using NzbDrone.Core.MediaFiles.Commands; using NzbDrone.Core.MediaFiles.EpisodeImport; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; using NzbDrone.Test.Common; @@ -22,7 +23,7 @@ namespace NzbDrone.Core.Test.MediaFiles public class DownloadedEpisodesImportServiceFixture : CoreTest { private string[] _subFolders = new[] { "c:\\root\\foldername".AsOsAgnostic() }; - private string[] _videoFiles = new[] { "c:\\root\\foldername\\video.ext".AsOsAgnostic() }; + private string[] _videoFiles = new[] { "c:\\root\\foldername\\30.rock.s01e01.ext".AsOsAgnostic() }; [SetUp] public void Setup() @@ -113,6 +114,8 @@ namespace NzbDrone.Core.Test.MediaFiles Mocker.GetMock() .Verify(v => v.GetSeries(It.IsAny()), Times.Never()); + + ExceptionVerification.ExpectedWarns(1); } [Test] @@ -129,7 +132,33 @@ namespace NzbDrone.Core.Test.MediaFiles } [Test] - public void should_delete_folder_if_files_were_imported() + public void should_delete_folder_if_files_were_imported_and_video_files_remain() + { + GivenValidSeries(); + + var localEpisode = new LocalEpisode(); + + var imported = new List(); + imported.Add(new ImportDecision(localEpisode)); + + Mocker.GetMock() + .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), true, null)) + .Returns(imported); + + Mocker.GetMock() + .Setup(s => s.Import(It.IsAny>(), true)) + .Returns(imported); + + Subject.Execute(new DownloadedEpisodesScanCommand()); + + Mocker.GetMock() + .Verify(v => v.DeleteFolder(It.IsAny(), true), Times.Never()); + + ExceptionVerification.ExpectedWarns(1); + } + + [Test] + public void should_delete_folder_if_files_were_imported_and_only_sample_files_remain() { GivenValidSeries(); @@ -146,6 +175,14 @@ namespace NzbDrone.Core.Test.MediaFiles .Setup(s => s.Import(It.IsAny>(), true)) .Returns(imported); + Mocker.GetMock() + .Setup(s => s.IsSample(It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(true); + Subject.Execute(new DownloadedEpisodesScanCommand()); Mocker.GetMock() diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/SampleServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/SampleServiceFixture.cs new file mode 100644 index 000000000..60db510f1 --- /dev/null +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/SampleServiceFixture.cs @@ -0,0 +1,151 @@ +using System; +using System.Linq; +using FizzWare.NBuilder; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.MediaFiles.EpisodeImport; +using NzbDrone.Core.MediaFiles.MediaInfo; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport +{ + [TestFixture] + public class SampleServiceFixture : CoreTest + { + private Series _series; + private LocalEpisode _localEpisode; + + [SetUp] + public void Setup() + { + _series = Builder.CreateNew() + .With(s => s.SeriesType = SeriesTypes.Standard) + .Build(); + + var episodes = Builder.CreateListOfSize(1) + .All() + .With(e => e.SeasonNumber = 1) + .Build() + .ToList(); + + _localEpisode = new LocalEpisode + { + Path = @"C:\Test\30 Rock\30.rock.s01e01.avi", + Episodes = episodes, + Series = _series, + Quality = new QualityModel(Quality.HDTV720p) + }; + } + + private void GivenFileSize(long size) + { + _localEpisode.Size = size; + } + + private void GivenRuntime(int seconds) + { + Mocker.GetMock() + .Setup(s => s.GetRunTime(It.IsAny())) + .Returns(new TimeSpan(0, 0, seconds)); + } + + [Test] + public void should_return_true_if_series_is_daily() + { + _series.SeriesType = SeriesTypes.Daily; + ShouldBeFalse(); + } + + [Test] + public void should_return_true_if_season_zero() + { + _localEpisode.Episodes[0].SeasonNumber = 0; + ShouldBeFalse(); + } + + [Test] + public void should_return_true_for_flv() + { + _localEpisode.Path = @"C:\Test\some.show.s01e01.flv"; + + ShouldBeFalse(); + + Mocker.GetMock().Verify(c => c.GetRunTime(It.IsAny()), Times.Never()); + } + + [Test] + public void should_use_runtime() + { + GivenRuntime(120); + GivenFileSize(1000.Megabytes()); + + Subject.IsSample(_localEpisode.Series, + _localEpisode.Quality, + _localEpisode.Path, + _localEpisode.Size, + _localEpisode.SeasonNumber); + + Mocker.GetMock().Verify(v => v.GetRunTime(It.IsAny()), Times.Once()); + } + + [Test] + public void should_return_false_if_runtime_is_less_than_minimum() + { + GivenRuntime(60); + + ShouldBeTrue(); + } + + [Test] + public void should_return_true_if_runtime_greater_than_than_minimum() + { + GivenRuntime(120); + + ShouldBeFalse(); + } + + [Test] + public void should_fall_back_to_file_size_if_mediainfo_dll_not_found_acceptable_size() + { + Mocker.GetMock() + .Setup(s => s.GetRunTime(It.IsAny())) + .Throws(); + + GivenFileSize(1000.Megabytes()); + ShouldBeFalse(); + } + + [Test] + public void should_fall_back_to_file_size_if_mediainfo_dll_not_found_undersize() + { + Mocker.GetMock() + .Setup(s => s.GetRunTime(It.IsAny())) + .Throws(); + + GivenFileSize(1.Megabytes()); + ShouldBeTrue(); + } + + private void ShouldBeTrue() + { + Subject.IsSample(_localEpisode.Series, + _localEpisode.Quality, + _localEpisode.Path, + _localEpisode.Size, + _localEpisode.SeasonNumber).Should().BeTrue(); + } + + private void ShouldBeFalse() + { + Subject.IsSample(_localEpisode.Series, + _localEpisode.Quality, + _localEpisode.Path, + _localEpisode.Size, + _localEpisode.SeasonNumber).Should().BeFalse(); + } + } +} diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotSampleSpecificationFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotSampleSpecificationFixture.cs index 9088e46ee..ab3e5d1fa 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotSampleSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotSampleSpecificationFixture.cs @@ -41,96 +41,11 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications }; } - private void GivenFileSize(long size) - { - _localEpisode.Size = size; - } - - private void GivenRuntime(int seconds) - { - Mocker.GetMock() - .Setup(s => s.GetRunTime(It.IsAny())) - .Returns(new TimeSpan(0, 0, seconds)); - } - - [Test] - public void should_return_true_if_series_is_daily() - { - _series.SeriesType = SeriesTypes.Daily; - Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue(); - } - - [Test] - public void should_return_true_if_season_zero() - { - _localEpisode.Episodes[0].SeasonNumber = 0; - Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue(); - } - [Test] public void should_return_true_for_existing_file() { _localEpisode.ExistingFile = true; Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue(); } - - [Test] - public void should_return_true_for_flv() - { - _localEpisode.Path = @"C:\Test\some.show.s01e01.flv"; - - Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue(); - - Mocker.GetMock().Verify(c => c.GetRunTime(It.IsAny()), Times.Never()); - } - - [Test] - public void should_use_runtime() - { - GivenRuntime(120); - GivenFileSize(1000.Megabytes()); - - Subject.IsSatisfiedBy(_localEpisode); - - Mocker.GetMock().Verify(v => v.GetRunTime(It.IsAny()), Times.Once()); - } - - [Test] - public void should_return_false_if_runtime_is_less_than_minimum() - { - GivenRuntime(60); - - Subject.IsSatisfiedBy(_localEpisode).Should().BeFalse(); - } - - [Test] - public void should_return_true_if_runtime_greater_than_than_minimum() - { - GivenRuntime(120); - - Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue(); - } - - [Test] - public void should_fall_back_to_file_size_if_mediainfo_dll_not_found_acceptable_size() - { - Mocker.GetMock() - .Setup(s => s.GetRunTime(It.IsAny())) - .Throws(); - - GivenFileSize(1000.Megabytes()); - Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue(); - } - - [Test] - public void should_fall_back_to_file_size_if_mediainfo_dll_not_found_undersize() - { - Mocker.GetMock() - .Setup(s => s.GetRunTime(It.IsAny())) - .Throws(); - - GivenFileSize(1.Megabytes()); - Subject.IsSatisfiedBy(_localEpisode).Should().BeFalse(); - } } } diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 6a967392b..022d01d8d 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -156,6 +156,7 @@ + diff --git a/src/NzbDrone.Core/MediaFiles/Commands/DownloadedEpisodesScanCommand.cs b/src/NzbDrone.Core/MediaFiles/Commands/DownloadedEpisodesScanCommand.cs index 189178b32..40066c875 100644 --- a/src/NzbDrone.Core/MediaFiles/Commands/DownloadedEpisodesScanCommand.cs +++ b/src/NzbDrone.Core/MediaFiles/Commands/DownloadedEpisodesScanCommand.cs @@ -1,3 +1,4 @@ +using System; using NzbDrone.Core.Messaging.Commands; namespace NzbDrone.Core.MediaFiles.Commands @@ -12,6 +13,7 @@ namespace NzbDrone.Core.MediaFiles.Commands } } - public bool SendUpdates { get; set; } + public Boolean SendUpdates { get; set; } + public String Path { get; set; } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs b/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs index b5531b32b..62423f203 100644 --- a/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using NLog; using NzbDrone.Common; using NzbDrone.Common.Disk; +using NzbDrone.Common.EnsureThat; using NzbDrone.Core.Configuration; using NzbDrone.Core.MediaFiles.Commands; using NzbDrone.Core.MediaFiles.EpisodeImport; @@ -24,6 +26,7 @@ namespace NzbDrone.Core.MediaFiles private readonly IConfigService _configService; private readonly IMakeImportDecision _importDecisionMaker; private readonly IImportApprovedEpisodes _importApprovedEpisodes; + private readonly ISampleService _sampleService; private readonly Logger _logger; public DownloadedEpisodesImportService(IDiskProvider diskProvider, @@ -33,6 +36,7 @@ namespace NzbDrone.Core.MediaFiles IConfigService configService, IMakeImportDecision importDecisionMaker, IImportApprovedEpisodes importApprovedEpisodes, + ISampleService sampleService, Logger logger) { _diskProvider = diskProvider; @@ -42,6 +46,7 @@ namespace NzbDrone.Core.MediaFiles _configService = configService; _importDecisionMaker = importDecisionMaker; _importApprovedEpisodes = importApprovedEpisodes; + _sampleService = sampleService; _logger = logger; } @@ -64,24 +69,7 @@ namespace NzbDrone.Core.MediaFiles foreach (var subFolder in _diskProvider.GetDirectories(downloadedEpisodesFolder)) { - try - { - if (_seriesService.SeriesPathExists(subFolder)) - { - continue; - } - - var importedFiles = ProcessSubFolder(new DirectoryInfo(subFolder)); - - if (importedFiles.Any()) - { - _diskProvider.DeleteFolder(subFolder, true); - } - } - catch (Exception e) - { - _logger.ErrorException("An error has occurred while importing folder: " + subFolder, e); - } + ProcessFolder(subFolder); } foreach (var videoFile in _diskScanService.GetVideoFiles(downloadedEpisodesFolder, false)) @@ -97,9 +85,9 @@ namespace NzbDrone.Core.MediaFiles } } - private List ProcessSubFolder(DirectoryInfo subfolderInfo) + private List ProcessFolder(DirectoryInfo directoryInfo) { - var cleanedUpName = GetCleanedUpFolderName(subfolderInfo.Name); + var cleanedUpName = GetCleanedUpFolderName(directoryInfo.Name); var series = _parsingService.GetSeries(cleanedUpName); var quality = QualityParser.ParseQuality(cleanedUpName); _logger.Debug("{0} folder quality: {1}", cleanedUpName, quality); @@ -110,7 +98,7 @@ namespace NzbDrone.Core.MediaFiles return new List(); } - var videoFiles = _diskScanService.GetVideoFiles(subfolderInfo.FullName); + var videoFiles = _diskScanService.GetVideoFiles(directoryInfo.FullName); return ProcessFiles(series, quality, videoFiles); } @@ -140,6 +128,33 @@ namespace NzbDrone.Core.MediaFiles return _importApprovedEpisodes.Import(decisions, true); } + private void ProcessFolder(string path) + { + Ensure.That(path, () => path).IsValidPath(); + + try + { + if (_seriesService.SeriesPathExists(path)) + { + _logger.Warn("Unable to process folder that contains sorted TV Shows"); + return; + } + + var directoryFolderInfo = new DirectoryInfo(path); + var importedFiles = ProcessFolder(directoryFolderInfo); + + if (importedFiles.Any() && ShouldDeleteFolder(directoryFolderInfo)) + { + _logger.Debug("Deleting folder after importing valid files"); + _diskProvider.DeleteFolder(path, true); + } + } + catch (Exception e) + { + _logger.ErrorException("An error has occurred while importing folder: " + path, e); + } + } + private string GetCleanedUpFolderName(string folder) { folder = folder.Replace("_UNPACK_", "") @@ -148,9 +163,47 @@ namespace NzbDrone.Core.MediaFiles return folder; } + private bool ShouldDeleteFolder(DirectoryInfo directoryInfo) + { + var videoFiles = _diskScanService.GetVideoFiles(directoryInfo.FullName); + var cleanedUpName = GetCleanedUpFolderName(directoryInfo.Name); + var series = _parsingService.GetSeries(cleanedUpName); + + foreach (var videoFile in videoFiles) + { + var episodeParseResult = Parser.Parser.ParseTitle(Path.GetFileName(videoFile)); + + if (episodeParseResult == null) + { + _logger.Warn("Unable to parse file on import: [{0}]", videoFile); + return false; + } + + var size = _diskProvider.GetFileSize(videoFile); + var quality = QualityParser.ParseQuality(videoFile); + + if (!_sampleService.IsSample(series, quality, videoFile, size, + episodeParseResult.SeasonNumber)) + { + _logger.Warn("Non-sample file has not been imported: [{0}]", videoFile); + return false; + } + } + + return true; + } + public void Execute(DownloadedEpisodesScanCommand message) { - ProcessDownloadedEpisodesFolder(); + if (message.Path.IsNullOrWhiteSpace()) + { + ProcessDownloadedEpisodesFolder(); + } + + else + { + ProcessFolder(message.Path); + } } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/SampleService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/SampleService.cs new file mode 100644 index 000000000..2e5e647c5 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/SampleService.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.IO; +using NLog; +using NzbDrone.Core.MediaFiles.MediaInfo; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.MediaFiles.EpisodeImport +{ + public interface ISampleService + { + bool IsSample(Series series, QualityModel quality, string path, long size, int seasonNumber); + } + + public class SampleService : ISampleService + { + private readonly IVideoFileInfoReader _videoFileInfoReader; + private readonly Logger _logger; + + private static List _largeSampleSizeQualities = new List { Quality.HDTV1080p, Quality.WEBDL1080p, Quality.Bluray1080p }; + + public SampleService(IVideoFileInfoReader videoFileInfoReader, Logger logger) + { + _videoFileInfoReader = videoFileInfoReader; + _logger = logger; + } + + public static long SampleSizeLimit + { + get + { + return 70.Megabytes(); + } + } + + public bool IsSample(Series series, QualityModel quality, string path, long size, int seasonNumber) + { + if (series.SeriesType == SeriesTypes.Daily) + { + _logger.Debug("Daily Series, skipping sample check"); + return false; + } + + if (seasonNumber == 0) + { + _logger.Debug("Special, skipping sample check"); + return false; + } + + var extension = Path.GetExtension(path); + + if (extension != null && extension.Equals(".flv", StringComparison.InvariantCultureIgnoreCase)) + { + _logger.Debug("Skipping sample check for .flv file"); + return false; + } + + try + { + var runTime = _videoFileInfoReader.GetRunTime(path); + + if (runTime.TotalMinutes.Equals(0)) + { + _logger.Error("[{0}] has a runtime of 0, is it a valid video file?", path); + return true; + } + + if (runTime.TotalSeconds < 90) + { + _logger.Debug("[{0}] appears to be a sample. Size: {1} Runtime: {2}", path, size, runTime); + return true; + } + } + + catch (DllNotFoundException) + { + _logger.Debug("Falling back to file size detection"); + + return CheckSize(size, quality); + } + + _logger.Debug("Runtime is over 90 seconds"); + return false; + } + + private bool CheckSize(long size, QualityModel quality) + { + if (_largeSampleSizeQualities.Contains(quality.Quality)) + { + if (size < SampleSizeLimit * 2) + { + _logger.Debug("1080p file is less than sample limit"); + return true; + } + } + + if (size < SampleSizeLimit) + { + _logger.Debug("File is less than sample limit"); + return true; + } + + return false; + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotSampleSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotSampleSpecification.cs index 11c7ce7cd..4101ba986 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotSampleSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotSampleSpecification.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.IO; using NLog; -using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.MediaFiles.MediaInfo; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; @@ -12,25 +11,16 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications { public class NotSampleSpecification : IImportDecisionEngineSpecification { - private readonly IVideoFileInfoReader _videoFileInfoReader; + private readonly ISampleService _sampleService; private readonly Logger _logger; - private static List _largeSampleSizeQualities = new List { Quality.HDTV1080p, Quality.WEBDL1080p, Quality.Bluray1080p }; - public NotSampleSpecification(IVideoFileInfoReader videoFileInfoReader, + public NotSampleSpecification(ISampleService sampleService, Logger logger) { - _videoFileInfoReader = videoFileInfoReader; + _sampleService = sampleService; _logger = logger; } - public static long SampleSizeLimit - { - get - { - return 70.Megabytes(); - } - } - public string RejectionReason { get { return "Sample"; } } public bool IsSatisfiedBy(LocalEpisode localEpisode) @@ -41,72 +31,11 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications return true; } - if (localEpisode.Series.SeriesType == SeriesTypes.Daily) - { - _logger.Debug("Daily Series, skipping sample check"); - return true; - } - - if (localEpisode.SeasonNumber == 0) - { - _logger.Debug("Special, skipping sample check"); - return true; - } - - var extension = Path.GetExtension(localEpisode.Path); - - if (extension != null && extension.Equals(".flv", StringComparison.InvariantCultureIgnoreCase)) - { - _logger.Debug("Skipping sample check for .flv file"); - return true; - } - - try - { - var runTime = _videoFileInfoReader.GetRunTime(localEpisode.Path); - - if (runTime.TotalMinutes.Equals(0)) - { - _logger.Error("[{0}] has a runtime of 0, is it a valid video file?", localEpisode); - return false; - } - - if (runTime.TotalSeconds < 90) - { - _logger.Debug("[{0}] appears to be a sample. Size: {1} Runtime: {2}", localEpisode.Path, localEpisode.Size, runTime); - return false; - } - } - - catch (DllNotFoundException) - { - _logger.Debug("Falling back to file size detection"); - - return CheckSize(localEpisode); - } - - _logger.Debug("Runtime is over 90 seconds"); - return true; - } - - private bool CheckSize(LocalEpisode localEpisode) - { - if (_largeSampleSizeQualities.Contains(localEpisode.Quality.Quality)) - { - if (localEpisode.Size < SampleSizeLimit * 2) - { - _logger.Debug("1080p file is less than sample limit"); - return false; - } - } - - if (localEpisode.Size < SampleSizeLimit) - { - _logger.Debug("File is less than sample limit"); - return false; - } - - return true; + return !_sampleService.IsSample(localEpisode.Series, + localEpisode.Quality, + localEpisode.Path, + localEpisode.Size, + localEpisode.SeasonNumber); } } } diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index c331efd94..df565832a 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -327,6 +327,7 @@ +