diff --git a/NzbDrone.Core.Test/ProviderTests/DiskScanProviderTests/ImportFileFixture.cs b/NzbDrone.Core.Test/ProviderTests/DiskScanProviderTests/ImportFileFixture.cs index fb0f789e7..efe2bff74 100644 --- a/NzbDrone.Core.Test/ProviderTests/DiskScanProviderTests/ImportFileFixture.cs +++ b/NzbDrone.Core.Test/ProviderTests/DiskScanProviderTests/ImportFileFixture.cs @@ -324,7 +324,7 @@ public void import_new_multi_part_file_episode_replace_two_files() } [Test] - public void import_new_episode_no_existing_episode_file() + public void should_import_new_episode_no_existing_episode_file() { const string fileName = "WEEDS.S03E01E02.DUAL.bluray.x264.AC3.-HELLYWOOD.mkv"; @@ -354,48 +354,6 @@ public void import_new_episode_no_existing_episode_file() Mocker.GetMock().Verify(p => p.DeleteFile(It.IsAny()), Times.Never()); } - [Test] - public void should_return_null_if_file_size_is_under_40MB() - { - var series = Builder - .CreateNew() - .Build(); - - const string path = @"C:\Test\TV\30.rock.s01e01.pilot.avi"; - - Mocker.GetMock() - .Setup(m => m.Exists(path)) - .Returns(false); - - Mocker.GetMock() - .Setup(d => d.GetSize(path)) - .Returns(20.Megabytes()); - - Mocker.Resolve().ImportFile(series, path).Should().BeNull(); - } - - private static void VerifyFileImport(EpisodeFile result, AutoMoqer Mocker, Episode fakeEpisode, long size) - { - result.Should().NotBeNull(); - result.SeriesId.Should().Be(fakeEpisode.SeriesId); - result.Size.Should().Be(size); - result.DateAdded.Should().HaveDay(DateTime.Now.Day); - Mocker.GetMock().Verify(p => p.Add(It.IsAny()), Times.Once()); - - //Get the count of episodes linked - var count = Mocker.GetMock().Object.GetEpisodesByParseResult(null).Count; - - Mocker.GetMock().Verify(p => p.UpdateEpisode(It.Is(e => e.EpisodeFileId == result.EpisodeFileId)), Times.Exactly(count)); - } - - private static void VerifySkipImport(EpisodeFile result, AutoMoqer Mocker) - { - result.Should().BeNull(); - Mocker.GetMock().Verify(p => p.Add(It.IsAny()), Times.Never()); - Mocker.GetMock().Verify(p => p.UpdateEpisode(It.IsAny()), Times.Never()); - Mocker.GetMock().Verify(p => p.DeleteFile(It.IsAny()), Times.Never()); - } - [Test] public void should_set_parseResult_SceneSource_if_not_in_series_Path() { @@ -441,5 +399,115 @@ public void should_not_set_parseResult_SceneSource_if_in_series_Path() Mocker.Verify(s => s.GetEpisodesByParseResult(It.Is(p => p.SceneSource == false)), Times.Once()); } + + [Test] + public void should_return_null_if_file_size_is_under_70MB_and_runTime_under_8_minutes() + { + var series = Builder + .CreateNew() + .Build(); + + const string path = @"C:\Test\TV\30.rock.s01e01.pilot.avi"; + + Mocker.GetMock() + .Setup(m => m.Exists(path)) + .Returns(false); + + Mocker.GetMock() + .Setup(d => d.GetSize(path)) + .Returns(20.Megabytes()); + + Mocker.GetMock() + .Setup(s => s.GetRunTime(path)) + .Returns(300); + + Mocker.Resolve().ImportFile(series, path).Should().BeNull(); + } + + [Test] + public void should_import_if_file_size_is_under_70MB_but_runTime_over_8_minutes() + { + var fakeSeries = Builder.CreateNew().Build(); + + var fakeEpisode = Builder.CreateNew() + .With(e => e.EpisodeFileId = 0) + .With(e => e.EpisodeFile = null) + .Build(); + + const string path = @"C:\Test\TV\30.rock.s01e01.pilot.avi"; + + Mocker.GetMock() + .Setup(m => m.Exists(path)) + .Returns(false); + + Mocker.GetMock() + .Setup(d => d.GetSize(path)) + .Returns(20.Megabytes()); + + Mocker.GetMock() + .Setup(s => s.GetRunTime(path)) + .Returns(600); + + Mocker.GetMock() + .Setup(e => e.GetEpisodesByParseResult(It.IsAny())).Returns(new List { fakeEpisode }); + + var result = Mocker.Resolve().ImportFile(fakeSeries, path); + + VerifyFileImport(result, Mocker, fakeEpisode, 20.Megabytes()); + Mocker.GetMock().Verify(p => p.DeleteFile(It.IsAny()), Times.Never()); + } + + [Test] + public void should_import_if_file_size_is_over_70MB_but_runTime_under_8_minutes() + { + With80MBFile(); + + var fakeSeries = Builder.CreateNew().Build(); + + var fakeEpisode = Builder.CreateNew() + .With(e => e.EpisodeFileId = 0) + .With(e => e.EpisodeFile = null) + .Build(); + + const string path = @"C:\Test\TV\30.rock.s01e01.pilot.avi"; + + Mocker.GetMock() + .Setup(m => m.Exists(path)) + .Returns(false); + + Mocker.GetMock() + .Setup(s => s.GetRunTime(path)) + .Returns(600); + + Mocker.GetMock() + .Setup(e => e.GetEpisodesByParseResult(It.IsAny())).Returns(new List { fakeEpisode }); + + var result = Mocker.Resolve().ImportFile(fakeSeries, path); + + VerifyFileImport(result, Mocker, fakeEpisode, SIZE); + Mocker.GetMock().Verify(p => p.DeleteFile(It.IsAny()), Times.Never()); + } + + private static void VerifyFileImport(EpisodeFile result, AutoMoqer Mocker, Episode fakeEpisode, long size) + { + result.Should().NotBeNull(); + result.SeriesId.Should().Be(fakeEpisode.SeriesId); + result.Size.Should().Be(size); + result.DateAdded.Should().HaveDay(DateTime.Now.Day); + Mocker.GetMock().Verify(p => p.Add(It.IsAny()), Times.Once()); + + //Get the count of episodes linked + var count = Mocker.GetMock().Object.GetEpisodesByParseResult(null).Count; + + Mocker.GetMock().Verify(p => p.UpdateEpisode(It.Is(e => e.EpisodeFileId == result.EpisodeFileId)), Times.Exactly(count)); + } + + private static void VerifySkipImport(EpisodeFile result, AutoMoqer Mocker) + { + result.Should().BeNull(); + Mocker.GetMock().Verify(p => p.Add(It.IsAny()), Times.Never()); + Mocker.GetMock().Verify(p => p.UpdateEpisode(It.IsAny()), Times.Never()); + Mocker.GetMock().Verify(p => p.DeleteFile(It.IsAny()), Times.Never()); + } } } diff --git a/NzbDrone.Core/MediaInfo.dll b/NzbDrone.Core/MediaInfo.dll new file mode 100644 index 000000000..d2e18aed2 Binary files /dev/null and b/NzbDrone.Core/MediaInfo.dll differ diff --git a/NzbDrone.Core/Model/MediaInfoModel.cs b/NzbDrone.Core/Model/MediaInfoModel.cs new file mode 100644 index 000000000..b75cf638b --- /dev/null +++ b/NzbDrone.Core/Model/MediaInfoModel.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Model +{ + public class MediaInfoModel + { + public string VideoCodec { get; set; } + public int VideoBitrate { get; set; } + public int Width { get; set; } + public int Height { get; set; } + public string AudioFormat { get; set; } + public int AudioBitrate { get; set; } + public int RunTime { get; set; } + public int AudioStreamCount { get; set; } + public int AudioChannels { get; set; } + public string AudioProfile { get; set; } + public decimal VideoFps { get; set; } + public string AudioLanguages { get; set; } + public string Subtitles { get; set; } + public string ScanType { get; set; } + } +} diff --git a/NzbDrone.Core/NzbDrone.Core.csproj b/NzbDrone.Core/NzbDrone.Core.csproj index 1a2061c76..086df3053 100644 --- a/NzbDrone.Core/NzbDrone.Core.csproj +++ b/NzbDrone.Core/NzbDrone.Core.csproj @@ -145,6 +145,9 @@ ..\packages\DotNetZip.1.9.1.8\lib\net20\Ionic.Zip.dll + + ..\packages\MediaInfoNet.0.3\lib\MediaInfoDotNet.dll + True @@ -277,6 +280,7 @@ + @@ -314,6 +318,7 @@ + @@ -618,6 +623,9 @@ + + Always + diff --git a/NzbDrone.Core/Providers/DiskScanProvider.cs b/NzbDrone.Core/Providers/DiskScanProvider.cs index c814dfd0a..bcb3c65f2 100644 --- a/NzbDrone.Core/Providers/DiskScanProvider.cs +++ b/NzbDrone.Core/Providers/DiskScanProvider.cs @@ -23,12 +23,13 @@ public class DiskScanProvider private readonly SignalRProvider _signalRProvider; private readonly ConfigProvider _configProvider; private readonly RecycleBinProvider _recycleBinProvider; + private readonly MediaInfoProvider _mediaInfoProvider; public DiskScanProvider(DiskProvider diskProvider, EpisodeProvider episodeProvider, SeriesProvider seriesProvider, MediaFileProvider mediaFileProvider, ExternalNotificationProvider externalNotificationProvider, DownloadProvider downloadProvider, SignalRProvider signalRProvider, ConfigProvider configProvider, - RecycleBinProvider recycleBinProvider) + RecycleBinProvider recycleBinProvider, MediaInfoProvider mediaInfoProvider) { _diskProvider = diskProvider; _episodeProvider = episodeProvider; @@ -39,6 +40,7 @@ public DiskScanProvider(DiskProvider diskProvider, EpisodeProvider episodeProvid _signalRProvider = signalRProvider; _configProvider = configProvider; _recycleBinProvider = recycleBinProvider; + _mediaInfoProvider = mediaInfoProvider; } public DiskScanProvider() @@ -110,10 +112,9 @@ public virtual EpisodeFile ImportFile(Series series, string filePath) } long size = _diskProvider.GetSize(filePath); - - //Todo: We shouldn't skip on file size alone, 64MB Family Guy episodes are skipped... - //Skip any file under 70MB - New samples don't even have sample in the name... - if (size < Constants.IgnoreFileSize) + var runTime = _mediaInfoProvider.GetRunTime(filePath); + + if(size < Constants.IgnoreFileSize && runTime < 480) { Logger.Trace("[{0}] appears to be a sample. skipping.", filePath); return null; diff --git a/NzbDrone.Core/Providers/MediaInfoProvider.cs b/NzbDrone.Core/Providers/MediaInfoProvider.cs new file mode 100644 index 000000000..7cde3afed --- /dev/null +++ b/NzbDrone.Core/Providers/MediaInfoProvider.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using MediaInfoLib; +using NLog; +using NzbDrone.Common; +using NzbDrone.Core.Model; + +namespace NzbDrone.Core.Providers +{ + public class MediaInfoProvider + { + private readonly DiskProvider _diskProvider; + + private static readonly Logger logger = LogManager.GetCurrentClassLogger(); + + public MediaInfoProvider(DiskProvider diskProvider) + { + _diskProvider = diskProvider; + } + + public MediaInfoProvider() + { + } + + public virtual MediaInfoModel GetMediaInfo(string filename) + { + if (!_diskProvider.FileExists(filename)) + throw new FileNotFoundException("Media file does not exist: " + filename); + + var mediaInfo = new MediaInfo(); + + try + { + logger.Trace("Getting media info from {0}", filename); + + mediaInfo.Option("ParseSpeed", "0.2"); + int open = mediaInfo.Open(filename); + + if (open != 0) + { + int width; + int height; + int videoBitRate; + int audioBitRate; + int runTime; + int streamCount; + int audioChannels; + + string subtitles = mediaInfo.Get(StreamKind.General, 0, "Text_Language_List"); + string scanType = mediaInfo.Get(StreamKind.Video, 0, "ScanType"); + Int32.TryParse(mediaInfo.Get(StreamKind.Video, 0, "Width"), out width); + Int32.TryParse(mediaInfo.Get(StreamKind.Video, 0, "Height"), out height); + Int32.TryParse(mediaInfo.Get(StreamKind.Video, 0, "BitRate"), out videoBitRate); + + string aBitRate = mediaInfo.Get(StreamKind.Audio, 0, "BitRate"); + int ABindex = aBitRate.IndexOf(" /"); + if (ABindex > 0) + aBitRate = aBitRate.Remove(ABindex); + + Int32.TryParse(aBitRate, out audioBitRate); + Int32.TryParse(mediaInfo.Get(StreamKind.General, 0, "PlayTime"), out runTime); + Int32.TryParse(mediaInfo.Get(StreamKind.Audio, 0, "StreamCount"), out streamCount); + + string audioChannelsStr = mediaInfo.Get(StreamKind.Audio, 0, "Channel(s)"); + int ACindex = audioChannelsStr.IndexOf(" /"); + if (ACindex > 0) + audioChannelsStr = audioChannelsStr.Remove(ACindex); + + string audioLanguages = mediaInfo.Get(StreamKind.General, 0, "Audio_Language_List"); + decimal videoFrameRate = Decimal.Parse(mediaInfo.Get(StreamKind.Video, 0, "FrameRate")); + string audioProfile = mediaInfo.Get(StreamKind.Audio, 0, "Format_Profile"); + + int APindex = audioProfile.IndexOf(" /"); + if (APindex > 0) + audioProfile = audioProfile.Remove(APindex); + + Int32.TryParse(audioChannelsStr, out audioChannels); + var mediaInfoModel = new MediaInfoModel + { + VideoCodec = mediaInfo.Get(StreamKind.Video, 0, "Codec/String"), + VideoBitrate = videoBitRate, + Height = height, + Width = width, + AudioFormat = mediaInfo.Get(StreamKind.Audio, 0, "Format"), + AudioBitrate = audioBitRate, + RunTime = (runTime / 1000), //InSeconds + AudioStreamCount = streamCount, + AudioChannels = audioChannels, + AudioProfile = audioProfile.Trim(), + VideoFps = videoFrameRate, + AudioLanguages = audioLanguages, + Subtitles = subtitles, + ScanType = scanType + }; + + mediaInfo.Close(); + return mediaInfoModel; + } + } + catch (Exception ex) + { + logger.ErrorException("Unable to parse media info from file: " + filename, ex); + mediaInfo.Close(); + } + + return null; + } + + public virtual Int32 GetRunTime(string filename) + { + var mediaInfo = new MediaInfo(); + + try + { + logger.Trace("Getting media info from {0}", filename); + + mediaInfo.Option("ParseSpeed", "0.2"); + int open = mediaInfo.Open(filename); + + if (open != 0) + { + int runTime; + Int32.TryParse(mediaInfo.Get(StreamKind.General, 0, "PlayTime"), out runTime); + + mediaInfo.Close(); + return runTime / 1000; //Convert to seconds + } + } + catch (Exception ex) + { + logger.ErrorException("Unable to parse media info from file: " + filename, ex); + mediaInfo.Close(); + } + + return 0; + } + } +} diff --git a/NzbDrone.Core/packages.config b/NzbDrone.Core/packages.config index 9672ac959..ff8a17e15 100644 --- a/NzbDrone.Core/packages.config +++ b/NzbDrone.Core/packages.config @@ -4,6 +4,7 @@ + diff --git a/NzbDrone.Services/NzbDrone.Services.Service/Web.config b/NzbDrone.Services/NzbDrone.Services.Service/Web.config index 64d264fb2..d93876ae7 100644 --- a/NzbDrone.Services/NzbDrone.Services.Service/Web.config +++ b/NzbDrone.Services/NzbDrone.Services.Service/Web.config @@ -16,7 +16,7 @@ - + diff --git a/NzbDrone.Web/Web.config b/NzbDrone.Web/Web.config index 7c9e049a3..fca40058e 100644 --- a/NzbDrone.Web/Web.config +++ b/NzbDrone.Web/Web.config @@ -72,7 +72,7 @@ - +