Episode import uses specs and moves before import now

pull/3113/head
Mark McDowall 12 years ago
parent 9ed5a06504
commit aeb8ee06f6

@ -199,7 +199,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
[Test] [Test]
public void should_return_unknown_series_rejection_if_series_is_unknow() public void should_return_unknown_series_rejection_if_series_is_unknown()
{ {
GivenSpecifications(_pass1, _pass2, _pass3); GivenSpecifications(_pass1, _pass2, _pass3);

@ -55,7 +55,7 @@ namespace NzbDrone.Core.Test.MediaFileTests
.Setup(e => e.BuildFilePath(It.IsAny<Series>(), fakeEpisode.First().SeasonNumber, filename, ".avi")) .Setup(e => e.BuildFilePath(It.IsAny<Series>(), fakeEpisode.First().SeasonNumber, filename, ".avi"))
.Returns(fi); .Returns(fi);
var result = Subject.MoveEpisodeFile(file, false); var result = Subject.MoveEpisodeFile(file);
result.Should().BeNull(); result.Should().BeNull();
} }
@ -106,7 +106,7 @@ namespace NzbDrone.Core.Test.MediaFileTests
.Setup(s => s.FileExists(currentFilename)) .Setup(s => s.FileExists(currentFilename))
.Returns(true); .Returns(true);
var result = Subject.MoveEpisodeFile(file, true); var result = Subject.MoveEpisodeFile(file);
} }
@ -153,7 +153,7 @@ namespace NzbDrone.Core.Test.MediaFileTests
.Setup(e => e.BuildFilePath(It.IsAny<Series>(), fakeEpisode.First().SeasonNumber, filename, ".mkv")) .Setup(e => e.BuildFilePath(It.IsAny<Series>(), fakeEpisode.First().SeasonNumber, filename, ".mkv"))
.Returns(fi); .Returns(fi);
var result = Subject.MoveEpisodeFile(file, true); var result = Subject.MoveEpisodeFile(file);
result.Should().BeNull(); result.Should().BeNull();
ExceptionVerification.ExpectedErrors(1); ExceptionVerification.ExpectedErrors(1);

@ -0,0 +1,155 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.MediaFiles.EpisodeImport;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.MediaFileTests.EpisodeImportTests
{
[TestFixture]
public class ImportDecisionMakerFixture : CoreTest<ImportDecisionMaker>
{
private List<String> _videoFiles;
private LocalEpisode _localEpisode;
private Series _series;
private Mock<IImportDecisionEngineSpecification> _pass1;
private Mock<IImportDecisionEngineSpecification> _pass2;
private Mock<IImportDecisionEngineSpecification> _pass3;
private Mock<IImportDecisionEngineSpecification> _fail1;
private Mock<IImportDecisionEngineSpecification> _fail2;
private Mock<IImportDecisionEngineSpecification> _fail3;
[SetUp]
public void Setup()
{
_pass1 = new Mock<IImportDecisionEngineSpecification>();
_pass2 = new Mock<IImportDecisionEngineSpecification>();
_pass3 = new Mock<IImportDecisionEngineSpecification>();
_fail1 = new Mock<IImportDecisionEngineSpecification>();
_fail2 = new Mock<IImportDecisionEngineSpecification>();
_fail3 = new Mock<IImportDecisionEngineSpecification>();
_pass1.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalEpisode>())).Returns(true);
_pass1.Setup(c => c.RejectionReason).Returns("_pass1");
_pass2.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalEpisode>())).Returns(true);
_pass2.Setup(c => c.RejectionReason).Returns("_pass2");
_pass3.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalEpisode>())).Returns(true);
_pass3.Setup(c => c.RejectionReason).Returns("_pass3");
_fail1.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalEpisode>())).Returns(false);
_fail1.Setup(c => c.RejectionReason).Returns("_fail1");
_fail2.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalEpisode>())).Returns(false);
_fail2.Setup(c => c.RejectionReason).Returns("_fail2");
_fail3.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalEpisode>())).Returns(false);
_fail3.Setup(c => c.RejectionReason).Returns("_fail3");
_videoFiles = new List<String> { "The.Office.S03E115.DVDRip.XviD-OSiTV" };
_series = new Series();
_localEpisode = new LocalEpisode { Series = _series, Path = @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV.avi" };
Mocker.GetMock<IParsingService>().Setup(c => c.GetEpisodes(It.IsAny<String>(), It.IsAny<Series>()))
.Returns(_localEpisode);
}
private void GivenSpecifications(params Mock<IImportDecisionEngineSpecification>[] mocks)
{
Mocker.SetConstant<IEnumerable<IRejectWithReason>>(mocks.Select(c => c.Object));
}
[Test]
public void should_call_all_specifications()
{
GivenSpecifications(_pass1, _pass2, _pass3, _fail1, _fail2, _fail3);
Subject.GetImportDecisions(_videoFiles, new Series());
_fail1.Verify(c => c.IsSatisfiedBy(_localEpisode), Times.Once());
_fail2.Verify(c => c.IsSatisfiedBy(_localEpisode), Times.Once());
_fail3.Verify(c => c.IsSatisfiedBy(_localEpisode), Times.Once());
_pass1.Verify(c => c.IsSatisfiedBy(_localEpisode), Times.Once());
_pass2.Verify(c => c.IsSatisfiedBy(_localEpisode), Times.Once());
_pass3.Verify(c => c.IsSatisfiedBy(_localEpisode), Times.Once());
}
[Test]
public void should_return_rejected_if_single_specs_fail()
{
GivenSpecifications(_fail1);
var result = Subject.GetImportDecisions(_videoFiles, new Series());
result.Single().Approved.Should().BeFalse();
}
[Test]
public void should_return_rejected_if_one_of_specs_fail()
{
GivenSpecifications(_pass1, _fail1, _pass2, _pass3);
var result = Subject.GetImportDecisions(_videoFiles, new Series());
result.Single().Approved.Should().BeFalse();
}
[Test]
public void should_return_pass_if_all_specs_pass()
{
GivenSpecifications(_pass1, _pass2, _pass3);
var result = Subject.GetImportDecisions(_videoFiles, new Series());
result.Single().Approved.Should().BeTrue();
}
[Test]
public void should_have_same_number_of_rejections_as_specs_that_failed()
{
GivenSpecifications(_pass1, _pass2, _pass3, _fail1, _fail2, _fail3);
var result = Subject.GetImportDecisions(_videoFiles, new Series());
result.Single().Rejections.Should().HaveCount(3);
}
[Test]
public void failed_parse_shouldnt_blowup_the_process()
{
GivenSpecifications(_pass1);
Mocker.GetMock<IParsingService>().Setup(c => c.GetEpisodes(It.IsAny<String>(), It.IsAny<Series>()))
.Throws<TestException>();
_videoFiles = new List<String>
{
"The.Office.S03E115.DVDRip.XviD-OSiTV",
"The.Office.S03E115.DVDRip.XviD-OSiTV",
"The.Office.S03E115.DVDRip.XviD-OSiTV"
};
Subject.GetImportDecisions(_videoFiles, new Series());
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetEpisodes(It.IsAny<String>(), It.IsAny<Series>()), Times.Exactly(_videoFiles.Count));
ExceptionVerification.ExpectedErrors(3);
}
}
}

@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.EpisodeImport.Specifications;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.MediaFileTests.EpisodeImportTests
{
[TestFixture]
public class NotAlreadyImportedSpecificationFixture : CoreTest<NotAlreadyImportedSpecification>
{
private LocalEpisode _localEpisode;
[SetUp]
public void Setup()
{
_localEpisode = new LocalEpisode
{
Path = @"C:\Test\30 Rock\30.rock.s01e01.avi"
};
}
[Test]
public void should_return_false_if_path_is_already_in_episodeFiles()
{
Mocker.GetMock<IMediaFileService>()
.Setup(s => s.Exists(_localEpisode.Path))
.Returns(true);
Subject.IsSatisfiedBy(_localEpisode).Should().BeFalse();
}
[Test]
public void should_return_true_if_new_file()
{
Mocker.GetMock<IMediaFileService>()
.Setup(s => s.Exists(_localEpisode.Path))
.Returns(false);
Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue();
}
}
}

@ -0,0 +1,122 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.EpisodeImport.Specifications;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Providers;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Test.MediaFileTests.EpisodeImportTests
{
[TestFixture]
public class NotSampleSpecificationFixture : CoreTest<NotSampleSpecification>
{
private Series _series;
private LocalEpisode _localEpisode;
[SetUp]
public void Setup()
{
_series = Builder<Series>.CreateNew()
.With(s => s.SeriesType = SeriesTypes.Standard)
.Build();
var episodes = Builder<Episode>.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
};
}
private void WithDailySeries()
{
_series.SeriesType = SeriesTypes.Daily;
}
private void WithSeasonZero()
{
_localEpisode.Episodes[0].SeasonNumber = 0;
}
private void WithFileSize(long size)
{
Mocker.GetMock<IDiskProvider>()
.Setup(s => s.GetFileSize(It.IsAny<String>()))
.Returns(size);
}
private void WithLength(int minutes)
{
Mocker.GetMock<IVideoFileInfoReader>()
.Setup(s => s.GetRunTime(It.IsAny<String>()))
.Returns(new TimeSpan(0, 0, minutes, 0));
}
[Test]
public void should_return_true_if_series_is_daily()
{
WithDailySeries();
Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue();
}
[Test]
public void should_return_true_if_season_zero()
{
WithSeasonZero();
Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue();
}
[Test]
public void should_return_false_if_undersize_and_under_length()
{
WithFileSize(10.Megabytes());
WithLength(1);
Subject.IsSatisfiedBy(_localEpisode).Should().BeFalse();
}
[Test]
public void should_return_true_if_undersize()
{
WithFileSize(10.Megabytes());
WithLength(10);
Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue();
}
[Test]
public void should_return_true_if_under_length()
{
WithFileSize(100.Megabytes());
WithLength(1);
Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue();
}
[Test]
public void should_return_true_if_over_size_and_length()
{
WithFileSize(100.Megabytes());
WithLength(10);
Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue();
}
}
}

@ -0,0 +1,159 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using FizzWare.NBuilder;
using FluentAssertions;
using Marr.Data;
using Moq;
using NUnit.Framework;
using NzbDrone.Common;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.EpisodeImport.Specifications;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Providers;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Test.MediaFileTests.EpisodeImportTests
{
[TestFixture]
public class UpgradeSpecificationFixture : CoreTest<UpgradeSpecification>
{
private Series _series;
private LocalEpisode _localEpisode;
[SetUp]
public void Setup()
{
_series = Builder<Series>.CreateNew()
.With(s => s.SeriesType = SeriesTypes.Standard)
.Build();
_localEpisode = new LocalEpisode
{
Path = @"C:\Test\30 Rock\30.rock.s01e01.avi",
Quality = new QualityModel(Quality.HDTV720p, false)
};
}
[Test]
public void should_return_true_if_no_existing_episodeFile()
{
_localEpisode.Episodes = Builder<Episode>.CreateListOfSize(1)
.All()
.With(e => e.EpisodeFileId = 0)
.With(e => e.EpisodeFile = null)
.Build()
.ToList();
Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue();
}
[Test]
public void should_return_true_if_no_existing_episodeFile_for_multi_episodes()
{
_localEpisode.Episodes = Builder<Episode>.CreateListOfSize(2)
.All()
.With(e => e.EpisodeFileId = 0)
.With(e => e.EpisodeFile = null)
.Build()
.ToList();
Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue();
}
[Test]
public void should_return_true_if_upgrade_for_existing_episodeFile()
{
_localEpisode.Episodes = Builder<Episode>.CreateListOfSize(1)
.All()
.With(e => e.EpisodeFileId = 1)
.With(e => e.EpisodeFile = new LazyLoaded<EpisodeFile>(
new EpisodeFile
{
Quality = new QualityModel(Quality.SDTV, false)
}))
.Build()
.ToList();
Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue();
}
[Test]
public void should_return_true_if_upgrade_for_existing_episodeFile_for_multi_episodes()
{
_localEpisode.Episodes = Builder<Episode>.CreateListOfSize(2)
.All()
.With(e => e.EpisodeFileId = 1)
.With(e => e.EpisodeFile = new LazyLoaded<EpisodeFile>(
new EpisodeFile
{
Quality = new QualityModel(Quality.SDTV, false)
}))
.Build()
.ToList();
Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue();
}
[Test]
public void should_return_false_if_not_an_upgrade_for_existing_episodeFile()
{
_localEpisode.Episodes = Builder<Episode>.CreateListOfSize(1)
.All()
.With(e => e.EpisodeFileId = 1)
.With(e => e.EpisodeFile = new LazyLoaded<EpisodeFile>(
new EpisodeFile
{
Quality = new QualityModel(Quality.Bluray720p, false)
}))
.Build()
.ToList();
Subject.IsSatisfiedBy(_localEpisode).Should().BeFalse();
}
[Test]
public void should_return_false_if_not_an_upgrade_for_existing_episodeFile_for_multi_episodes()
{
_localEpisode.Episodes = Builder<Episode>.CreateListOfSize(2)
.All()
.With(e => e.EpisodeFileId = 1)
.With(e => e.EpisodeFile = new LazyLoaded<EpisodeFile>(
new EpisodeFile
{
Quality = new QualityModel(Quality.Bluray720p, false)
}))
.Build()
.ToList();
Subject.IsSatisfiedBy(_localEpisode).Should().BeFalse();
}
[Test]
public void should_return_false_if_not_an_upgrade_for_one_existing_episodeFile_for_multi_episode()
{
_localEpisode.Episodes = Builder<Episode>.CreateListOfSize(2)
.TheFirst(1)
.With(e => e.EpisodeFileId = 1)
.With(e => e.EpisodeFile = new LazyLoaded<EpisodeFile>(
new EpisodeFile
{
Quality = new QualityModel(Quality.SDTV, false)
}))
.TheNext(1)
.With(e => e.EpisodeFileId = 2)
.With(e => e.EpisodeFile = new LazyLoaded<EpisodeFile>(
new EpisodeFile
{
Quality = new QualityModel(Quality.Bluray720p, false)
}))
.Build()
.ToList();
Subject.IsSatisfiedBy(_localEpisode).Should().BeFalse();
}
}
}

@ -148,6 +148,10 @@
<Compile Include="JobTests\TestJobs.cs" /> <Compile Include="JobTests\TestJobs.cs" />
<Compile Include="MediaCoverTests\CoverExistsSpecificationFixture.cs" /> <Compile Include="MediaCoverTests\CoverExistsSpecificationFixture.cs" />
<Compile Include="MediaCoverTests\MediaCoverServiceFixture.cs" /> <Compile Include="MediaCoverTests\MediaCoverServiceFixture.cs" />
<Compile Include="MediaFileTests\EpisodeImportTests\UpgradeSpecificationFixture.cs" />
<Compile Include="MediaFileTests\EpisodeImportTests\NotSampleSpecificationFixture.cs" />
<Compile Include="MediaFileTests\EpisodeImportTests\ImportDecisionMakerFixture.cs" />
<Compile Include="MediaFileTests\EpisodeImportTests\NotAlreadyImportedSpecificationFixture.cs" />
<Compile Include="MediaFileTests\MediaFileTableCleanupServiceFixture.cs" /> <Compile Include="MediaFileTests\MediaFileTableCleanupServiceFixture.cs" />
<Compile Include="MediaFileTests\MediaFileRepositoryFixture.cs" /> <Compile Include="MediaFileTests\MediaFileRepositoryFixture.cs" />
<Compile Include="MediaFileTests\EpisodeFileMoverFixture.cs" /> <Compile Include="MediaFileTests\EpisodeFileMoverFixture.cs" />
@ -200,7 +204,6 @@
<Compile Include="DecisionEngineTests\AcceptableSizeSpecificationFixture.cs" /> <Compile Include="DecisionEngineTests\AcceptableSizeSpecificationFixture.cs" />
<Compile Include="Qualities\QualitySizeServiceFixture.cs" /> <Compile Include="Qualities\QualitySizeServiceFixture.cs" />
<Compile Include="TvTests\EpisodeProviderTests\EpisodeProviderTest_GetEpisodesByParseResult.cs" /> <Compile Include="TvTests\EpisodeProviderTests\EpisodeProviderTest_GetEpisodesByParseResult.cs" />
<Compile Include="ProviderTests\DiskScanProviderTests\ImportFileFixture.cs" />
<Compile Include="FluentTest.cs" /> <Compile Include="FluentTest.cs" />
<Compile Include="InstrumentationTests\DatabaseTargetFixture.cs" /> <Compile Include="InstrumentationTests\DatabaseTargetFixture.cs" />
<Compile Include="OrganizerTests\GetNewFilenameFixture.cs" /> <Compile Include="OrganizerTests\GetNewFilenameFixture.cs" />

@ -1,303 +0,0 @@
using System;
using System.Linq;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Providers;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.ProviderTests.DiskScanProviderTests
{
public class ImportFileFixture : CoreTest<DiskScanService>
{
private long _fileSize = 80.Megabytes();
private Series _fakeSeries;
private Episode[] _fakeEpisodes;
private Episode _fakeEpisode;
[SetUp]
public void Setup()
{
_fakeSeries = Builder<Series>
.CreateNew()
.Build();
_fakeEpisode = Builder<Episode>
.CreateNew()
.With(c => c.EpisodeFileId = 0)
.Build();
_fakeEpisodes = Builder<Episode>.CreateListOfSize(2)
.All()
.With(c => c.SeasonNumber = 3)
.With(c => c.EpisodeFileId = 1)
.With(e => e.EpisodeFile = new EpisodeFile())
.BuildList().ToArray();
GivenNewFile();
GivenVideoDuration(TimeSpan.FromMinutes(20));
GivenFileSize(_fileSize);
}
private void GivenFileSize(long size)
{
_fileSize = size;
Mocker.GetMock<IDiskProvider>()
.Setup(d => d.GetFileSize(It.IsAny<String>()))
.Returns(size);
}
private void GivenVideoDuration(TimeSpan duration)
{
Mocker.GetMock<IVideoFileInfoReader>()
.Setup(d => d.GetRunTime(It.IsAny<String>()))
.Returns(duration);
}
private void GivenEpisodes(Episode[] episodes, QualityModel quality)
{
foreach (var episode in episodes)
{
if (episode.EpisodeFile == null)
{
episode.EpisodeFileId = 0;
}
else
{
episode.EpisodeFileId = episode.EpisodeFile.Value.Id;
}
}
Mocker.GetMock<IParsingService>()
.Setup(c => c.GetEpisodes(It.IsAny<string>(), It.IsAny<Series>()))
.Returns(new LocalEpisode
{
Episodes = episodes.ToList(),
Quality = quality
});
}
private void GivenNewFile()
{
Mocker.GetMock<IMediaFileService>()
.Setup(p => p.Exists(It.IsAny<String>()))
.Returns(false);
}
[Test]
public void import_new_file_should_succeed()
{
GivenEpisodes(new[] { _fakeEpisode }, new QualityModel());
var result = Subject.ImportFile(_fakeSeries, "file.ext");
VerifyFileImport(result);
}
[Test]
public void import_new_file_with_same_quality_should_succeed()
{
_fakeEpisode.EpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.SDTV) };
GivenEpisodes(new[] { _fakeEpisode }, new QualityModel(Quality.SDTV));
var result = Subject.ImportFile(_fakeSeries, "file.ext");
VerifyFileImport(result);
}
[Test]
public void import_new_file_with_better_quality_should_succeed()
{
_fakeEpisode.EpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.SDTV) };
GivenEpisodes(new[] { _fakeEpisode }, new QualityModel(Quality.HDTV1080p));
var result = Subject.ImportFile(_fakeSeries, "file.ext");
VerifyFileImport(result);
}
[Test]
public void import_new_file_episode_has_better_quality_should_skip()
{
_fakeEpisode.EpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.HDTV1080p), Id = 1 };
GivenEpisodes(new[] { _fakeEpisode }, new QualityModel(Quality.SDTV));
var result = Subject.ImportFile(_fakeSeries, "file.ext");
VerifySkipImport(result);
}
[Test]
public void import_unparsable_file_should_skip()
{
Mocker.GetMock<IParsingService>()
.Setup(c => c.GetEpisodes(It.IsAny<string>(), It.IsAny<Series>()))
.Returns<LocalEpisode>(null);
var result = Subject.ImportFile(_fakeSeries, "file.ext");
VerifySkipImport(result);
}
[Test]
public void import_existing_file_should_skip()
{
Mocker.GetMock<IMediaFileService>()
.Setup(p => p.Exists(It.IsAny<String>()))
.Returns(true);
var result = Subject.ImportFile(_fakeSeries, "file.ext");
VerifySkipImport(result);
}
[Test]
public void import_file_with_no_episode_in_db_should_skip()
{
GivenEpisodes(new Episode[0], new QualityModel());
var result = Subject.ImportFile(_fakeSeries, "file.ext");
VerifySkipImport(result);
}
[Test]
public void import_new_multi_part_file_episode_with_better_quality_than_existing()
{
_fakeEpisodes[0].EpisodeFile = new EpisodeFile();
_fakeEpisodes[1].EpisodeFile = new EpisodeFile();
_fakeEpisodes[0].EpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.SDTV) };
_fakeEpisodes[1].EpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.SDTV) };
GivenEpisodes(_fakeEpisodes, new QualityModel(Quality.HDTV1080p));
var result = Subject.ImportFile(_fakeSeries, "file.ext");
VerifyFileImport(result);
}
[Test]
public void skip_import_new_multi_part_file_episode_existing_has_better_quality()
{
_fakeEpisodes[0].EpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.HDTV1080p), Id = 1 };
_fakeEpisodes[1].EpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.HDTV1080p), Id = 1 };
GivenEpisodes(_fakeEpisodes, new QualityModel(Quality.SDTV));
var result = Subject.ImportFile(_fakeSeries, "file.ext");
VerifySkipImport(result);
}
[Test]
public void should_skip_if_file_size_is_under_70MB_and_runTime_under_3_minutes()
{
GivenFileSize(50.Megabytes());
GivenVideoDuration(TimeSpan.FromMinutes(1));
GivenEpisodes(new[] { _fakeEpisode }, new QualityModel(Quality.HDTV1080p));
var result = Subject.ImportFile(_fakeSeries, "file.ext");
VerifySkipImport(result);
}
[Test]
public void should_import_if_file_size_is_under_70MB_but_runTime_over_3_minutes()
{
GivenFileSize(50.Megabytes());
GivenVideoDuration(TimeSpan.FromMinutes(20));
GivenEpisodes(new[] { _fakeEpisode }, new QualityModel(Quality.HDTV1080p));
var result = Subject.ImportFile(_fakeSeries, "file.ext");
VerifyFileImport(result);
Mocker.GetMock<IDiskProvider>().Verify(p => p.DeleteFile(It.IsAny<string>()), Times.Never());
}
[Test]
public void should_import_if_file_size_is_over_70MB_but_runTime_under_3_minutes()
{
GivenFileSize(100.Megabytes());
GivenVideoDuration(TimeSpan.FromMinutes(1));
GivenEpisodes(new[] { _fakeEpisode }, new QualityModel(Quality.HDTV1080p));
var result = Subject.ImportFile(_fakeSeries, "file.ext");
VerifyFileImport(result);
}
[Test]
public void should_import_special_even_if_file_size_is_under_70MB_and_runTime_under_3_minutes()
{
GivenFileSize(10.Megabytes());
GivenVideoDuration(TimeSpan.FromMinutes(1));
_fakeEpisode.SeasonNumber = 0;
GivenEpisodes(new[] { _fakeEpisode }, new QualityModel(Quality.HDTV1080p));
var result = Subject.ImportFile(_fakeSeries, "file.ext");
VerifyFileImport(result);
}
[Test]
public void should_skip_if_daily_series_with_file_size_is_under_70MB_and_runTime_under_3_minutes()
{
GivenFileSize(10.Megabytes());
GivenVideoDuration(TimeSpan.FromMinutes(1));
_fakeEpisode.SeasonNumber = 0;
_fakeSeries.SeriesType = SeriesTypes.Daily;
GivenEpisodes(new[] { _fakeEpisode }, new QualityModel(Quality.HDTV1080p));
var result = Subject.ImportFile(_fakeSeries, "file.ext");
VerifySkipImport(result);
}
private void VerifyFileImport(EpisodeFile result)
{
result.Should().NotBeNull();
result.SeriesId.Should().Be(_fakeSeries.Id);
result.Size.Should().Be(_fileSize);
result.DateAdded.Should().HaveDay(DateTime.UtcNow.Day);
Mocker.GetMock<IMediaFileService>().Verify(c => c.Add(result), Times.Once());
}
private void VerifySkipImport(EpisodeFile result)
{
result.Should().BeNull();
Mocker.GetMock<IMediaFileService>().Verify(p => p.Add(It.IsAny<EpisodeFile>()), Times.Never());
}
}
}

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using FizzWare.NBuilder; using FizzWare.NBuilder;
using Moq; using Moq;
@ -6,6 +7,7 @@ using NUnit.Framework;
using NzbDrone.Common; using NzbDrone.Common;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.EpisodeImport;
using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Parser; using NzbDrone.Core.Parser;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
@ -26,7 +28,6 @@ namespace NzbDrone.Core.Test.ProviderTests.PostDownloadProviderTests
{ {
_fakeEpisodeFile = Builder<EpisodeFile>.CreateNew().Build(); _fakeEpisodeFile = Builder<EpisodeFile>.CreateNew().Build();
Mocker.GetMock<IDiskScanService>().Setup(c => c.GetVideoFiles(It.IsAny<string>(), It.IsAny<bool>())) Mocker.GetMock<IDiskScanService>().Setup(c => c.GetVideoFiles(It.IsAny<string>(), It.IsAny<bool>()))
.Returns(_videoFiles); .Returns(_videoFiles);
@ -37,24 +38,6 @@ namespace NzbDrone.Core.Test.ProviderTests.PostDownloadProviderTests
.Returns("c:\\drop\\"); .Returns("c:\\drop\\");
} }
private void WithOldWrite()
{
Mocker.GetMock<IDiskProvider>()
.Setup(c => c.GetLastFolderWrite(It.IsAny<String>()))
.Returns(DateTime.Now.AddDays(-5));
}
private void WithRecentFolderWrite()
{
Mocker.GetMock<IDiskProvider>()
.Setup(c => c.GetLastFolderWrite(It.IsAny<String>()))
.Returns(DateTime.UtcNow);
Mocker.GetMock<IDiskProvider>()
.Setup(c => c.GetLastFileWrite(It.IsAny<String>()))
.Returns(DateTime.UtcNow);
}
[Test] [Test]
public void should_import_file() public void should_import_file()
{ {
@ -66,65 +49,14 @@ namespace NzbDrone.Core.Test.ProviderTests.PostDownloadProviderTests
[Test] [Test]
public void should_search_for_series_using_folder_name() public void should_search_for_series_using_folder_name()
{ {
WithOldWrite();
Subject.ProcessDownloadedEpisodesFolder(); Subject.ProcessDownloadedEpisodesFolder();
Mocker.GetMock<IParsingService>().Verify(c => c.GetSeries("foldername"), Times.Once()); Mocker.GetMock<IParsingService>().Verify(c => c.GetSeries("foldername"), Times.Once());
}
[Test]
public void all_imported_files_should_be_moved()
{
Mocker.GetMock<IDiskScanService>().Setup(c => c.ImportFile(It.IsAny<Series>(), It.IsAny<string>()))
.Returns(_fakeEpisodeFile);
Subject.ProcessDownloadedEpisodesFolder();
Mocker.GetMock<IMoveEpisodeFiles>().Verify(c => c.MoveEpisodeFile(_fakeEpisodeFile, true), Times.Once());
}
[Test]
public void should_trigger_import_event_on_import()
{
Mocker.GetMock<IDiskScanService>().Setup(c => c.ImportFile(It.IsAny<Series>(), It.IsAny<string>()))
.Returns(_fakeEpisodeFile);
Subject.ProcessDownloadedEpisodesFolder();
VerifyEventPublished<EpisodeImportedEvent>();
} }
[Test] [Test]
public void should_not_attempt_move_if_nothing_is_imported() public void should_skip_if_file_is_in_use_by_another_process()
{ {
Mocker.GetMock<IDiskScanService>().Setup(c => c.ImportFile(It.IsAny<Series>(), It.IsAny<string>()))
.Returns<EpisodeFile>(null);
Subject.ProcessDownloadedEpisodesFolder();
Mocker.GetMock<IMoveEpisodeFiles>().Verify(c => c.MoveEpisodeFile(It.IsAny<EpisodeFile>(), It.IsAny<bool>()), Times.Never());
}
[Test]
public void should_not_publish_import_event_if_nothing_is_imported()
{
Mocker.GetMock<IDiskScanService>().Setup(c => c.ImportFile(It.IsAny<Series>(), It.IsAny<string>()))
.Returns<EpisodeFile>(null);
Subject.ProcessDownloadedEpisodesFolder();
VerifyEventNotPublished<EpisodeImportedEvent>();
}
[Test]
public void should_skip_if_folder_is_in_use_by_another_process()
{
Mocker.GetMock<IDiskProvider>().Setup(c => c.IsFileLocked(It.IsAny<FileInfo>())) Mocker.GetMock<IDiskProvider>().Setup(c => c.IsFileLocked(It.IsAny<FileInfo>()))
.Returns(true); .Returns(true);
@ -134,13 +66,13 @@ namespace NzbDrone.Core.Test.ProviderTests.PostDownloadProviderTests
private void VerifyNoImport() private void VerifyNoImport()
{ {
Mocker.GetMock<IDiskScanService>().Verify(c => c.ImportFile(It.IsAny<Series>(), It.IsAny<string>()), Mocker.GetMock<IImportApprovedEpisodes>().Verify(c => c.Import(It.IsAny<List<ImportDecision>>(), true),
Times.Never()); Times.Never());
} }
private void VerifyImport() private void VerifyImport()
{ {
Mocker.GetMock<IDiskScanService>().Verify(c => c.ImportFile(It.IsAny<Series>(), It.IsAny<string>()), Mocker.GetMock<IImportApprovedEpisodes>().Verify(c => c.Import(It.IsAny<List<ImportDecision>>(), true),
Times.Once()); Times.Once());
} }
} }

@ -6,6 +6,7 @@ using NLog;
using NzbDrone.Common; using NzbDrone.Common;
using NzbDrone.Common.Messaging; using NzbDrone.Common.Messaging;
using NzbDrone.Core.MediaFiles.Commands; using NzbDrone.Core.MediaFiles.Commands;
using NzbDrone.Core.MediaFiles.EpisodeImport;
using NzbDrone.Core.Parser; using NzbDrone.Core.Parser;
using NzbDrone.Core.Providers; using NzbDrone.Core.Providers;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
@ -15,7 +16,6 @@ namespace NzbDrone.Core.MediaFiles
{ {
public interface IDiskScanService public interface IDiskScanService
{ {
EpisodeFile ImportFile(Series series, string filePath);
string[] GetVideoFiles(string path, bool allDirectories = true); string[] GetVideoFiles(string path, bool allDirectories = true);
} }
@ -25,19 +25,20 @@ namespace NzbDrone.Core.MediaFiles
private static readonly string[] MediaExtensions = new[] { ".mkv", ".avi", ".wmv", ".mp4", ".mpg", ".mpeg", ".xvid", ".flv", ".mov", ".rm", ".rmvb", ".divx", ".dvr-ms", ".ts", ".ogm", ".m4v", ".strm" }; private static readonly string[] MediaExtensions = new[] { ".mkv", ".avi", ".wmv", ".mp4", ".mpg", ".mpeg", ".xvid", ".flv", ".mov", ".rm", ".rmvb", ".divx", ".dvr-ms", ".ts", ".ogm", ".m4v", ".strm" };
private readonly IDiskProvider _diskProvider; private readonly IDiskProvider _diskProvider;
private readonly ISeriesService _seriesService; private readonly ISeriesService _seriesService;
private readonly IMediaFileService _mediaFileService; private readonly IMakeImportDecision _importDecisionMaker;
private readonly IVideoFileInfoReader _videoFileInfoReader; private readonly IImportApprovedEpisodes _importApprovedEpisodes;
private readonly IParsingService _parsingService;
private readonly IMessageAggregator _messageAggregator; private readonly IMessageAggregator _messageAggregator;
public DiskScanService(IDiskProvider diskProvider, ISeriesService seriesService, IMediaFileService mediaFileService, IVideoFileInfoReader videoFileInfoReader, public DiskScanService(IDiskProvider diskProvider,
IParsingService parsingService, IMessageAggregator messageAggregator) ISeriesService seriesService,
IMakeImportDecision importDecisionMaker,
IImportApprovedEpisodes importApprovedEpisodes,
IMessageAggregator messageAggregator)
{ {
_diskProvider = diskProvider; _diskProvider = diskProvider;
_seriesService = seriesService; _seriesService = seriesService;
_mediaFileService = mediaFileService; _importDecisionMaker = importDecisionMaker;
_videoFileInfoReader = videoFileInfoReader; _importApprovedEpisodes = importApprovedEpisodes;
_parsingService = parsingService;
_messageAggregator = messageAggregator; _messageAggregator = messageAggregator;
} }
@ -53,71 +54,8 @@ namespace NzbDrone.Core.MediaFiles
var mediaFileList = GetVideoFiles(series.Path); var mediaFileList = GetVideoFiles(series.Path);
foreach (var filePath in mediaFileList) var decisions = _importDecisionMaker.GetImportDecisions(mediaFileList, series);
{ _importApprovedEpisodes.Import(decisions);
try
{
ImportFile(series, filePath);
}
catch (Exception e)
{
Logger.ErrorException("Couldn't import file " + filePath, e);
}
}
//Todo: Find the "best" episode file for all found episodes and import that one
//Todo: Move the episode linking to here, instead of import (or rename import)
}
public EpisodeFile ImportFile(Series series, string filePath)
{
Logger.Trace("Importing file to database [{0}]", filePath);
if (_mediaFileService.Exists(filePath))
{
Logger.Trace("[{0}] already exists in the database. skipping.", filePath);
return null;
}
var parsedEpisode = _parsingService.GetEpisodes(filePath, series);
if (parsedEpisode == null || !parsedEpisode.Episodes.Any())
{
return null;
}
var size = _diskProvider.GetFileSize(filePath);
if (series.SeriesType == SeriesTypes.Daily || parsedEpisode.SeasonNumber > 0)
{
var runTime = _videoFileInfoReader.GetRunTime(filePath);
if (size < Constants.IgnoreFileSize && runTime.TotalMinutes < 3)
{
Logger.Trace("[{0}] appears to be a sample. skipping.", filePath);
return null;
}
}
if (parsedEpisode.Episodes.Any(e => e.EpisodeFileId != 0 && e.EpisodeFile.Value.Quality > parsedEpisode.Quality))
{
Logger.Trace("This file isn't an upgrade for all episodes. Skipping {0}", filePath);
return null;
}
var episodeFile = new EpisodeFile();
episodeFile.DateAdded = DateTime.UtcNow;
episodeFile.SeriesId = series.Id;
episodeFile.Path = filePath.CleanPath();
episodeFile.Size = size;
episodeFile.Quality = parsedEpisode.Quality;
episodeFile.SeasonNumber = parsedEpisode.SeasonNumber;
episodeFile.SceneName = Path.GetFileNameWithoutExtension(filePath.CleanPath());
episodeFile.Episodes = parsedEpisode.Episodes;
//Todo: We shouldn't actually import the file until we confirm its the only one we want.
//Todo: Separate episodeFile creation from importing (pass file to import to import)
_mediaFileService.Add(episodeFile);
return episodeFile;
} }
public string[] GetVideoFiles(string path, bool allDirectories = true) public string[] GetVideoFiles(string path, bool allDirectories = true)

@ -1,11 +1,12 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using NLog; using NLog;
using NzbDrone.Common; using NzbDrone.Common;
using NzbDrone.Common.Messaging; using NzbDrone.Common.Messaging;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles.Commands; using NzbDrone.Core.MediaFiles.Commands;
using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.MediaFiles.EpisodeImport;
using NzbDrone.Core.Parser; using NzbDrone.Core.Parser;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
@ -19,7 +20,8 @@ namespace NzbDrone.Core.MediaFiles
private readonly IMoveEpisodeFiles _episodeFileMover; private readonly IMoveEpisodeFiles _episodeFileMover;
private readonly IParsingService _parsingService; private readonly IParsingService _parsingService;
private readonly IConfigService _configService; private readonly IConfigService _configService;
private readonly IMessageAggregator _messageAggregator; private readonly IMakeImportDecision _importDecisionMaker;
private readonly IImportApprovedEpisodes _importApprovedEpisodes;
private readonly Logger _logger; private readonly Logger _logger;
public DownloadedEpisodesImportService(IDiskProvider diskProvider, public DownloadedEpisodesImportService(IDiskProvider diskProvider,
@ -28,7 +30,8 @@ namespace NzbDrone.Core.MediaFiles
IMoveEpisodeFiles episodeFileMover, IMoveEpisodeFiles episodeFileMover,
IParsingService parsingService, IParsingService parsingService,
IConfigService configService, IConfigService configService,
IMessageAggregator messageAggregator, IMakeImportDecision importDecisionMaker,
IImportApprovedEpisodes importApprovedEpisodes,
Logger logger) Logger logger)
{ {
_diskProvider = diskProvider; _diskProvider = diskProvider;
@ -37,7 +40,8 @@ namespace NzbDrone.Core.MediaFiles
_episodeFileMover = episodeFileMover; _episodeFileMover = episodeFileMover;
_parsingService = parsingService; _parsingService = parsingService;
_configService = configService; _configService = configService;
_messageAggregator = messageAggregator; _importDecisionMaker = importDecisionMaker;
_importApprovedEpisodes = importApprovedEpisodes;
_logger = logger; _logger = logger;
} }
@ -92,7 +96,7 @@ namespace NzbDrone.Core.MediaFiles
} }
} }
public void ProcessSubFolder(DirectoryInfo subfolderInfo) private void ProcessSubFolder(DirectoryInfo subfolderInfo)
{ {
var series = _parsingService.GetSeries(subfolderInfo.Name); var series = _parsingService.GetSeries(subfolderInfo.Name);
@ -102,12 +106,9 @@ namespace NzbDrone.Core.MediaFiles
return; return;
} }
var files = _diskScanService.GetVideoFiles(subfolderInfo.FullName); var videoFiles = _diskScanService.GetVideoFiles(subfolderInfo.FullName);
foreach (var file in files) ProcessFiles(videoFiles, series);
{
ProcessVideoFile(file, series);
}
} }
private void ProcessVideoFile(string videoFile, Series series) private void ProcessVideoFile(string videoFile, Series series)
@ -118,13 +119,13 @@ namespace NzbDrone.Core.MediaFiles
return; return;
} }
var episodeFile = _diskScanService.ImportFile(series, videoFile); ProcessFiles(new [] { videoFile }, series);
}
if (episodeFile != null) private void ProcessFiles(IEnumerable<string> videoFiles, Series series)
{ {
_episodeFileMover.MoveEpisodeFile(episodeFile, true); var decisions = _importDecisionMaker.GetImportDecisions(videoFiles, series);
_messageAggregator.PublishEvent(new EpisodeImportedEvent(episodeFile)); _importApprovedEpisodes.Import(decisions, true);
}
} }
public void Execute(DownloadedEpisodesScanCommand message) public void Execute(DownloadedEpisodesScanCommand message)

@ -6,13 +6,15 @@ using NzbDrone.Common;
using NzbDrone.Common.Messaging; using NzbDrone.Common.Messaging;
using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Organizer; using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
namespace NzbDrone.Core.MediaFiles namespace NzbDrone.Core.MediaFiles
{ {
public interface IMoveEpisodeFiles public interface IMoveEpisodeFiles
{ {
EpisodeFile MoveEpisodeFile(EpisodeFile episodeFile, bool newDownload = false); EpisodeFile MoveEpisodeFile(EpisodeFile episodeFile);
EpisodeFile MoveEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEpisode);
} }
public class MoveEpisodeFiles : IMoveEpisodeFiles public class MoveEpisodeFiles : IMoveEpisodeFiles
@ -36,55 +38,68 @@ namespace NzbDrone.Core.MediaFiles
_logger = logger; _logger = logger;
} }
public EpisodeFile MoveEpisodeFile(EpisodeFile episodeFile, bool newDownload = false) public EpisodeFile MoveEpisodeFile(EpisodeFile episodeFile)
{ {
if (episodeFile == null) if (episodeFile == null)
throw new ArgumentNullException("episodeFile"); throw new ArgumentNullException("episodeFile");
var series = _seriesRepository.Get(episodeFile.SeriesId); var series = _seriesRepository.Get(episodeFile.SeriesId);
var episodes = _episodeService.GetEpisodesByFileId(episodeFile.Id); var episodes = _episodeService.GetEpisodesByFileId(episodeFile.Id);
string newFileName = _buildFileNames.BuildFilename(episodes, series, episodeFile); var newFileName = _buildFileNames.BuildFilename(episodes, series, episodeFile);
var newFile = _buildFileNames.BuildFilePath(series, episodes.First().SeasonNumber, newFileName, Path.GetExtension(episodeFile.Path)); var destinationFilename = _buildFileNames.BuildFilePath(series, episodes.First().SeasonNumber, newFileName, Path.GetExtension(episodeFile.Path));
//Only rename if existing and new filenames don't match episodeFile = MoveFile(episodeFile, destinationFilename);
if (DiskProvider.PathEquals(episodeFile.Path, newFile))
_mediaFileService.Update(episodeFile);
return episodeFile;
}
public EpisodeFile MoveEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEpisode)
{ {
_logger.Debug("Skipping file rename, source and destination are the same: {0}", episodeFile.Path); var newFileName = _buildFileNames.BuildFilename(localEpisode.Episodes, localEpisode.Series, episodeFile);
return null; var destinationFilename = _buildFileNames.BuildFilePath(localEpisode.Series, localEpisode.SeasonNumber, newFileName, Path.GetExtension(episodeFile.Path));
episodeFile = MoveFile(episodeFile, destinationFilename);
//TODO: This just re-parses the source path (which is how we got localEpisode to begin with)
var parsedEpisodeInfo = Parser.Parser.ParsePath(localEpisode.Path);
_messageAggregator.PublishEvent(new EpisodeDownloadedEvent(parsedEpisodeInfo, localEpisode.Series));
return episodeFile;
} }
private EpisodeFile MoveFile(EpisodeFile episodeFile, string destinationFilename)
{
if (!_diskProvider.FileExists(episodeFile.Path)) if (!_diskProvider.FileExists(episodeFile.Path))
{ {
_logger.Error("Episode file path does not exist, {0}", episodeFile.Path); _logger.Error("Episode file path does not exist, {0}", episodeFile.Path);
return null; return null;
} }
_diskProvider.CreateFolder(new FileInfo(newFile).DirectoryName); //Only rename if existing and new filenames don't match
if (DiskProvider.PathEquals(episodeFile.Path, destinationFilename))
{
_logger.Debug("Skipping file rename, source and destination are the same: {0}", episodeFile.Path);
return null;
}
_diskProvider.CreateFolder(new FileInfo(destinationFilename).DirectoryName);
_logger.Debug("Moving [{0}] > [{1}]", episodeFile.Path, newFile); _logger.Debug("Moving [{0}] > [{1}]", episodeFile.Path, destinationFilename);
_diskProvider.MoveFile(episodeFile.Path, newFile); _diskProvider.MoveFile(episodeFile.Path, destinationFilename);
//Wrapped in Try/Catch to prevent this from causing issues with remote NAS boxes, the move worked, which is more important. //Wrapped in Try/Catch to prevent this from causing issues with remote NAS boxes, the move worked, which is more important.
try try
{ {
_diskProvider.InheritFolderPermissions(newFile); _diskProvider.InheritFolderPermissions(destinationFilename);
} }
catch (UnauthorizedAccessException ex) catch (UnauthorizedAccessException ex)
{ {
_logger.Debug("Unable to apply folder permissions to: ", newFile); _logger.Debug("Unable to apply folder permissions to: ", destinationFilename);
_logger.TraceException(ex.Message, ex); _logger.TraceException(ex.Message, ex);
} }
episodeFile.Path = newFile; episodeFile.Path = destinationFilename;
_mediaFileService.Update(episodeFile);
var parsedEpisodeInfo = Parser.Parser.ParsePath(episodeFile.Path);
parsedEpisodeInfo.Quality = episodeFile.Quality;
if (newDownload)
{
_messageAggregator.PublishEvent(new EpisodeDownloadedEvent(parsedEpisodeInfo, series));
}
return episodeFile; return episodeFile;
} }

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.MediaFiles.EpisodeImport
{
public interface IImportDecisionEngineSpecification : IRejectWithReason
{
bool IsSatisfiedBy(LocalEpisode localEpisode);
}
}

@ -0,0 +1,94 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using NLog;
using NzbDrone.Common;
using NzbDrone.Common.Messaging;
using NzbDrone.Core.MediaFiles.Events;
namespace NzbDrone.Core.MediaFiles.EpisodeImport
{
public interface IImportApprovedEpisodes
{
List<ImportDecision> Import(List<ImportDecision> decisions, bool newDownloads = false);
}
public class ImportApprovedEpisodes : IImportApprovedEpisodes
{
private readonly IMoveEpisodeFiles _episodeFileMover;
private readonly MediaFileService _mediaFileService;
private readonly DiskProvider _diskProvider;
private readonly IMessageAggregator _messageAggregator;
private readonly Logger _logger;
public ImportApprovedEpisodes(IMoveEpisodeFiles episodeFileMover,
MediaFileService mediaFileService,
DiskProvider diskProvider,
IMessageAggregator messageAggregator,
Logger logger)
{
_episodeFileMover = episodeFileMover;
_mediaFileService = mediaFileService;
_diskProvider = diskProvider;
_messageAggregator = messageAggregator;
_logger = logger;
}
public List<ImportDecision> Import(List<ImportDecision> decisions, bool newDownload = false)
{
var qualifiedReports = GetQualifiedReports(decisions);
var imported = new List<ImportDecision>();
foreach (var report in qualifiedReports)
{
var localEpisode = report.LocalEpisode;
try
{
if (imported.SelectMany(r => r.LocalEpisode.Episodes)
.Select(e => e.Id)
.ToList()
.Intersect(localEpisode.Episodes.Select(e => e.Id))
.Any())
{
continue;
}
var episodeFile = new EpisodeFile();
episodeFile.DateAdded = DateTime.UtcNow;
episodeFile.SeriesId = localEpisode.Series.Id;
episodeFile.Path = localEpisode.Path.CleanPath();
episodeFile.Size = _diskProvider.GetFileSize(localEpisode.Path);
episodeFile.Quality = localEpisode.Quality;
episodeFile.SeasonNumber = localEpisode.SeasonNumber;
episodeFile.SceneName = Path.GetFileNameWithoutExtension(localEpisode.Path.CleanPath());
episodeFile.Episodes = localEpisode.Episodes;
if (newDownload)
{
episodeFile = _episodeFileMover.MoveEpisodeFile(episodeFile, localEpisode);
}
_mediaFileService.Add(episodeFile);
_messageAggregator.PublishEvent(new EpisodeImportedEvent(episodeFile));
}
catch (Exception e)
{
_logger.WarnException("Couldn't add report to download queue. " + localEpisode, e);
}
}
return imported;
}
private List<ImportDecision> GetQualifiedReports(List<ImportDecision> decisions)
{
return decisions.Where(c => c.Approved)
.OrderByDescending(c => c.LocalEpisode.Quality)
.ToList();
}
}
}

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.MediaFiles.EpisodeImport
{
public class ImportDecision
{
public LocalEpisode LocalEpisode { get; private set; }
public IEnumerable<string> Rejections { get; private set; }
public bool Approved
{
get
{
return !Rejections.Any();
}
}
public ImportDecision(LocalEpisode localEpisode, params string[] rejections)
{
LocalEpisode = localEpisode;
Rejections = rejections.ToList();
}
}
}

@ -0,0 +1,106 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NLog;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.MediaFiles.EpisodeImport
{
public interface IMakeImportDecision
{
List<ImportDecision> GetImportDecisions(IEnumerable<String> videoFiles, Series series);
}
public class ImportDecisionMaker : IMakeImportDecision
{
private readonly IEnumerable<IRejectWithReason> _specifications;
private readonly IParsingService _parsingService;
private readonly Logger _logger;
public ImportDecisionMaker(IEnumerable<IRejectWithReason> specifications, IParsingService parsingService, Logger logger)
{
_specifications = specifications;
_parsingService = parsingService;
_logger = logger;
}
public List<ImportDecision> GetImportDecisions(IEnumerable<String> videoFiles, Series series)
{
return GetDecisions(videoFiles, series).ToList();
}
private IEnumerable<ImportDecision> GetDecisions(IEnumerable<String> videoFiles, Series series)
{
foreach (var file in videoFiles)
{
ImportDecision decision = null;
try
{
var parsedEpisode = _parsingService.GetEpisodes(file, series);
if (parsedEpisode != null)
{
decision = GetDecision(parsedEpisode);
}
else
{
parsedEpisode = new LocalEpisode();
parsedEpisode.Path = file;
decision = new ImportDecision(parsedEpisode, "Unable to parse file");
}
}
catch (Exception e)
{
_logger.ErrorException("Couldn't process report.", e);
}
if (decision != null)
{
yield return decision;
}
}
}
private ImportDecision GetDecision(LocalEpisode localEpisode)
{
var reasons = _specifications.Select(c => EvaluateSpec(c, localEpisode))
.Where(c => !string.IsNullOrWhiteSpace(c));
return new ImportDecision(localEpisode, reasons.ToArray());
}
private string EvaluateSpec(IRejectWithReason spec, LocalEpisode localEpisode)
{
try
{
if (string.IsNullOrWhiteSpace(spec.RejectionReason))
{
throw new InvalidOperationException("[Need Rejection Text]");
}
var generalSpecification = spec as IImportDecisionEngineSpecification;
if (generalSpecification != null && !generalSpecification.IsSatisfiedBy(localEpisode))
{
return spec.RejectionReason;
}
}
catch (Exception e)
{
//e.Data.Add("report", remoteEpisode.Report.ToJson());
//e.Data.Add("parsed", remoteEpisode.ParsedEpisodeInfo.ToJson());
_logger.ErrorException("Couldn't evaluate decision on " + localEpisode.Path, e);
return string.Format("{0}: {1}", spec.GetType().Name, e.Message);
}
return null;
}
}
}

@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NLog;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
{
public class NotAlreadyImportedSpecification : IImportDecisionEngineSpecification
{
private readonly IMediaFileService _mediaFileService;
private readonly Logger _logger;
public NotAlreadyImportedSpecification(IMediaFileService mediaFileService, Logger logger)
{
_mediaFileService = mediaFileService;
_logger = logger;
}
public string RejectionReason { get { return "Is Sample"; } }
public bool IsSatisfiedBy(LocalEpisode localEpisode)
{
if (_mediaFileService.Exists(localEpisode.Path))
{
_logger.Trace("[{0}] already exists in the database. skipping.", localEpisode.Path);
return false;
}
return true;
}
}
}

@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NLog;
using NzbDrone.Common;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Providers;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
{
public class NotSampleSpecification : IImportDecisionEngineSpecification
{
private readonly IDiskProvider _diskProvider;
private readonly IVideoFileInfoReader _videoFileInfoReader;
private readonly Logger _logger;
public NotSampleSpecification(IDiskProvider diskProvider, IVideoFileInfoReader videoFileInfoReader, Logger logger)
{
_diskProvider = diskProvider;
_videoFileInfoReader = videoFileInfoReader;
_logger = logger;
}
public string RejectionReason { get { return "Sample"; } }
public bool IsSatisfiedBy(LocalEpisode localEpisode)
{
if (localEpisode.Series.SeriesType == SeriesTypes.Daily)
{
_logger.Trace("Daily Series, skipping sample check");
return true;
}
if (localEpisode.SeasonNumber == 0)
{
_logger.Trace("Special, skipping sample check");
return true;
}
var size = _diskProvider.GetFileSize(localEpisode.Path);
var runTime = _videoFileInfoReader.GetRunTime(localEpisode.Path);
if (size < Constants.IgnoreFileSize && runTime.TotalMinutes < 3)
{
_logger.Trace("[{0}] appears to be a sample.", localEpisode.Path);
return false;
}
return true;
}
}
}

@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NLog;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
{
public class UpgradeSpecification : IImportDecisionEngineSpecification
{
private readonly Logger _logger;
public UpgradeSpecification(Logger logger)
{
_logger = logger;
}
public string RejectionReason { get { return "Is Sample"; } }
public bool IsSatisfiedBy(LocalEpisode localEpisode)
{
if (localEpisode.Episodes.Any(e => e.EpisodeFileId != 0 && e.EpisodeFile.Value.Quality > localEpisode.Quality))
{
_logger.Trace("This file isn't an upgrade for all episodes. Skipping {0}", localEpisode.Path);
return false;
}
return true;
}
}
}

@ -274,6 +274,13 @@
<Compile Include="MediaFiles\Commands\CleanUpRecycleBinCommand.cs" /> <Compile Include="MediaFiles\Commands\CleanUpRecycleBinCommand.cs" />
<Compile Include="MediaFiles\Commands\DownloadedEpisodesScanCommand.cs" /> <Compile Include="MediaFiles\Commands\DownloadedEpisodesScanCommand.cs" />
<Compile Include="MediaFiles\Commands\DiskScanCommand.cs" /> <Compile Include="MediaFiles\Commands\DiskScanCommand.cs" />
<Compile Include="MediaFiles\EpisodeImport\ImportDecision.cs" />
<Compile Include="MediaFiles\EpisodeImport\IImportDecisionEngineSpecification.cs" />
<Compile Include="MediaFiles\EpisodeImport\ImportDecisionMaker.cs" />
<Compile Include="MediaFiles\EpisodeImport\ImportApprovedEpisodes.cs" />
<Compile Include="MediaFiles\EpisodeImport\Specifications\NotAlreadyImportedSpecification.cs" />
<Compile Include="MediaFiles\EpisodeImport\Specifications\UpgradeSpecification.cs" />
<Compile Include="MediaFiles\EpisodeImport\Specifications\NotSampleSpecification.cs" />
<Compile Include="MediaFiles\Events\EpisodeImportedEvent.cs" /> <Compile Include="MediaFiles\Events\EpisodeImportedEvent.cs" />
<Compile Include="MediaFiles\Events\EpisodeDownloadedEvent.cs" /> <Compile Include="MediaFiles\Events\EpisodeDownloadedEvent.cs" />
<Compile Include="Download\EpisodeGrabbedEvent.cs" /> <Compile Include="Download\EpisodeGrabbedEvent.cs" />

@ -6,11 +6,14 @@ namespace NzbDrone.Core.Parser.Model
{ {
public class LocalEpisode public class LocalEpisode
{ {
//public ParsedEpisodeInfo ParsedEpisodeInfo { get; set; } public Series Series { get; set; }
public List<Episode> Episodes { get; set; } public List<Episode> Episodes { get; set; }
public QualityModel Quality { get; set; } public QualityModel Quality { get; set; }
public int SeasonNumber { get { return Episodes.Select(c => c.SeasonNumber).Distinct().Single(); } } public int SeasonNumber { get { return Episodes.Select(c => c.SeasonNumber).Distinct().Single(); } }
public string Path { get; set; }
} }
} }

@ -26,7 +26,6 @@ namespace NzbDrone.Core.Parser
_logger = logger; _logger = logger;
} }
public LocalEpisode GetEpisodes(string fileName, Series series) public LocalEpisode GetEpisodes(string fileName, Series series)
{ {
var parsedEpisodeInfo = Parser.ParseTitle(fileName); var parsedEpisodeInfo = Parser.ParseTitle(fileName);
@ -45,8 +44,10 @@ namespace NzbDrone.Core.Parser
return new LocalEpisode return new LocalEpisode
{ {
Series = series,
Quality = parsedEpisodeInfo.Quality, Quality = parsedEpisodeInfo.Quality,
Episodes = episodes, Episodes = episodes,
Path = fileName
}; };
} }

Loading…
Cancel
Save