Episode import improvements

Fixed: Use folder name when file name is not parsable on import
pull/4/head
Mark McDowall 10 years ago
parent 5b54b02d7e
commit 20782bbbc1

@ -1,6 +1,6 @@
using NzbDrone.Common.Serializer;
namespace NzbDrone.Test.Common
namespace NzbDrone.Common.Extensions
{
public static class ObjectExtensions
{

@ -134,6 +134,7 @@
<Compile Include="Extensions\DateTimeExtensions.cs" />
<Compile Include="Crypto\HashConverter.cs" />
<Compile Include="Extensions\Int64Extensions.cs" />
<Compile Include="Extensions\ObjectExtensions.cs" />
<Compile Include="Extensions\StreamExtensions.cs" />
<Compile Include="Extensions\XmlExtentions.cs" />
<Compile Include="HashUtil.cs" />

@ -5,6 +5,7 @@ using Moq;
using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.History;
@ -153,8 +154,13 @@ namespace NzbDrone.Core.Test.Download
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>
{
new ImportResult(new ImportDecision(new LocalEpisode {Path = @"C:\TestPath\Droned.S01E01.mkv"}, "Rejected!"),"Test Failure"),
new ImportResult(new ImportDecision(new LocalEpisode {Path = @"C:\TestPath\Droned.S01E02.mkv"}, "Rejected!"),"Test Failure")
new ImportResult(
new ImportDecision(
new LocalEpisode {Path = @"C:\TestPath\Droned.S01E01.mkv"}, new Rejection("Rejected!")), "Test Failure"),
new ImportResult(
new ImportDecision(
new LocalEpisode {Path = @"C:\TestPath\Droned.S01E02.mkv"},new Rejection("Rejected!")), "Test Failure")
});
Subject.Process(_trackedDownload);

@ -4,6 +4,7 @@ using FizzWare.NBuilder;
using Marr.Data;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download.Pending;
using NzbDrone.Core.Parser;
@ -12,7 +13,6 @@ using NzbDrone.Core.Profiles;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
{

@ -4,17 +4,16 @@ using FizzWare.NBuilder;
using Marr.Data;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.Pending;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Profiles;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
{

@ -4,6 +4,7 @@ using FizzWare.NBuilder;
using Marr.Data;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.Pending;
@ -14,7 +15,6 @@ using NzbDrone.Core.Profiles;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
{

@ -101,7 +101,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
Subject.Scan(_series);
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _series, false, (QualityModel)null), Times.Once());
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _series), Times.Once());
}
[Test]
@ -119,7 +119,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
Subject.Scan(_series);
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _series, false, (QualityModel)null), Times.Once());
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _series), Times.Once());
}
[Test]
@ -141,7 +141,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
Subject.Scan(_series);
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 4), _series, false, (QualityModel)null), Times.Once());
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 4), _series), Times.Once());
}
[Test]
@ -160,7 +160,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
Subject.Scan(_series);
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _series, false, (QualityModel)null), Times.Once());
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _series), Times.Once());
}
}
}

@ -78,7 +78,7 @@ namespace NzbDrone.Core.Test.MediaFiles
Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory));
Mocker.GetMock<IMakeImportDecision>()
.Verify(c => c.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Series>(), It.IsAny<bool>(), It.IsAny<QualityModel>()),
.Verify(c => c.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Series>(), It.IsAny<ParsedEpisodeInfo>(), It.IsAny<bool>()),
Times.Never());
VerifyNoImport();
@ -129,7 +129,7 @@ namespace NzbDrone.Core.Test.MediaFiles
imported.Add(new ImportDecision(localEpisode));
Mocker.GetMock<IMakeImportDecision>()
.Setup(s => s.GetImportDecisions(It.IsAny<List<String>>(), It.IsAny<Series>(), true, null))
.Setup(s => s.GetImportDecisions(It.IsAny<List<String>>(), It.IsAny<Series>(), null, true))
.Returns(imported);
Mocker.GetMock<IImportApprovedEpisodes>()
@ -155,14 +155,14 @@ namespace NzbDrone.Core.Test.MediaFiles
imported.Add(new ImportDecision(localEpisode));
Mocker.GetMock<IMakeImportDecision>()
.Setup(s => s.GetImportDecisions(It.IsAny<List<String>>(), It.IsAny<Series>(), true, null))
.Setup(s => s.GetImportDecisions(It.IsAny<List<String>>(), It.IsAny<Series>(), null, true))
.Returns(imported);
Mocker.GetMock<IImportApprovedEpisodes>()
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true, null))
.Returns(imported.Select(i => new ImportResult(i)).ToList());
Mocker.GetMock<ISampleService>()
Mocker.GetMock<IDetectSample>()
.Setup(s => s.IsSample(It.IsAny<Series>(),
It.IsAny<QualityModel>(),
It.IsAny<String>(),
@ -224,14 +224,14 @@ namespace NzbDrone.Core.Test.MediaFiles
imported.Add(new ImportDecision(localEpisode));
Mocker.GetMock<IMakeImportDecision>()
.Setup(s => s.GetImportDecisions(It.IsAny<List<String>>(), It.IsAny<Series>(), true, null))
.Setup(s => s.GetImportDecisions(It.IsAny<List<String>>(), It.IsAny<Series>(), null, true))
.Returns(imported);
Mocker.GetMock<IImportApprovedEpisodes>()
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true, null))
.Returns(imported.Select(i => new ImportResult(i)).ToList());
Mocker.GetMock<ISampleService>()
Mocker.GetMock<IDetectSample>()
.Setup(s => s.IsSample(It.IsAny<Series>(),
It.IsAny<QualityModel>(),
It.IsAny<String>(),

@ -45,31 +45,20 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
_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");
_pass1.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalEpisode>())).Returns(Decision.Accept());
_pass2.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalEpisode>())).Returns(Decision.Accept());
_pass3.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalEpisode>())).Returns(Decision.Accept());
_pass2.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalEpisode>())).Returns(true);
_pass2.Setup(c => c.RejectionReason).Returns("_pass2");
_fail1.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalEpisode>())).Returns(Decision.Reject("_fail1"));
_fail2.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalEpisode>())).Returns(Decision.Reject("_fail2"));
_fail3.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalEpisode>())).Returns(Decision.Reject("_fail3"));
_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> { @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV.avi" };
_series = Builder<Series>.CreateNew()
.With(e => e.Profile = new Profile { Items = Qualities.QualityFixture.GetDefaultQualities() })
.Build();
_quality = new QualityModel(Quality.DVD);
_localEpisode = new LocalEpisode
{
Series = _series,
@ -78,17 +67,24 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
};
Mocker.GetMock<IParsingService>()
.Setup(c => c.GetLocalEpisode(It.IsAny<String>(), It.IsAny<Series>(), It.IsAny<Boolean>()))
.Setup(c => c.GetLocalEpisode(It.IsAny<String>(), It.IsAny<Series>(), It.IsAny<ParsedEpisodeInfo>(), It.IsAny<Boolean>()))
.Returns(_localEpisode);
Mocker.GetMock<IMediaFileService>()
.Setup(c => c.FilterExistingFiles(_videoFiles, It.IsAny<Series>()))
.Returns(_videoFiles);
GivenVideoFiles(new List<string> { @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV.avi".AsOsAgnostic() });
}
private void GivenSpecifications(params Mock<IImportDecisionEngineSpecification>[] mocks)
{
Mocker.SetConstant<IEnumerable<IRejectWithReason>>(mocks.Select(c => c.Object));
Mocker.SetConstant(mocks.Select(c => c.Object));
}
private void GivenVideoFiles(IEnumerable<string> videoFiles)
{
_videoFiles = videoFiles.ToList();
Mocker.GetMock<IMediaFileService>()
.Setup(c => c.FilterExistingFiles(_videoFiles, It.IsAny<Series>()))
.Returns(_videoFiles);
}
[Test]
@ -96,7 +92,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
{
GivenSpecifications(_pass1, _pass2, _pass3, _fail1, _fail2, _fail3);
Subject.GetImportDecisions(_videoFiles, new Series(), false);
Subject.GetImportDecisions(_videoFiles, new Series(), null, false);
_fail1.Verify(c => c.IsSatisfiedBy(_localEpisode), Times.Once());
_fail2.Verify(c => c.IsSatisfiedBy(_localEpisode), Times.Once());
@ -111,7 +107,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
{
GivenSpecifications(_fail1);
var result = Subject.GetImportDecisions(_videoFiles, new Series(), false);
var result = Subject.GetImportDecisions(_videoFiles, new Series());
result.Single().Approved.Should().BeFalse();
}
@ -121,7 +117,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
{
GivenSpecifications(_pass1, _fail1, _pass2, _pass3);
var result = Subject.GetImportDecisions(_videoFiles, new Series(), false);
var result = Subject.GetImportDecisions(_videoFiles, new Series());
result.Single().Approved.Should().BeFalse();
}
@ -131,7 +127,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
{
GivenSpecifications(_pass1, _pass2, _pass3);
var result = Subject.GetImportDecisions(_videoFiles, new Series(), false);
var result = Subject.GetImportDecisions(_videoFiles, new Series());
result.Single().Approved.Should().BeTrue();
}
@ -141,7 +137,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
{
GivenSpecifications(_pass1, _pass2, _pass3, _fail1, _fail2, _fail3);
var result = Subject.GetImportDecisions(_videoFiles, new Series(), false);
var result = Subject.GetImportDecisions(_videoFiles, new Series());
result.Single().Rejections.Should().HaveCount(3);
}
@ -151,7 +147,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
GivenSpecifications(_pass1);
Mocker.GetMock<IParsingService>()
.Setup(c => c.GetLocalEpisode(It.IsAny<String>(), It.IsAny<Series>(), It.IsAny<Boolean>()))
.Setup(c => c.GetLocalEpisode(It.IsAny<String>(), It.IsAny<Series>(), It.IsAny<ParsedEpisodeInfo>(), It.IsAny<Boolean>()))
.Throws<TestException>();
_videoFiles = new List<String>
@ -161,14 +157,12 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
"The.Office.S03E115.DVDRip.XviD-OSiTV"
};
Mocker.GetMock<IMediaFileService>()
.Setup(c => c.FilterExistingFiles(_videoFiles, It.IsAny<Series>()))
.Returns(_videoFiles);
GivenVideoFiles(_videoFiles);
Subject.GetImportDecisions(_videoFiles, _series, false);
Subject.GetImportDecisions(_videoFiles, _series);
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalEpisode(It.IsAny<String>(), It.IsAny<Series>(), It.IsAny<Boolean>()), Times.Exactly(_videoFiles.Count));
.Verify(c => c.GetLocalEpisode(It.IsAny<String>(), It.IsAny<Series>(), It.IsAny<ParsedEpisodeInfo>(), It.IsAny<Boolean>()), Times.Exactly(_videoFiles.Count));
ExceptionVerification.ExpectedErrors(3);
}
@ -179,7 +173,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
GivenSpecifications(_pass1, _pass2, _pass3);
var expectedQuality = QualityParser.ParseQuality(_videoFiles.Single());
var result = Subject.GetImportDecisions(_videoFiles, _series, false, null);
var result = Subject.GetImportDecisions(_videoFiles, _series);
result.Single().LocalEpisode.Quality.Should().Be(expectedQuality);
}
@ -190,7 +184,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
GivenSpecifications(_pass1, _pass2, _pass3);
var expectedQuality = QualityParser.ParseQuality(_videoFiles.Single());
var result = Subject.GetImportDecisions(_videoFiles, _series, false, new QualityModel(Quality.SDTV));
var result = Subject.GetImportDecisions(_videoFiles, _series, new ParsedEpisodeInfo{Quality = new QualityModel(Quality.SDTV)}, true);
result.Single().LocalEpisode.Quality.Should().Be(expectedQuality);
}
@ -201,7 +195,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
GivenSpecifications(_pass1, _pass2, _pass3);
var expectedQuality = new QualityModel(Quality.Bluray1080p);
var result = Subject.GetImportDecisions(_videoFiles, _series, false, expectedQuality);
var result = Subject.GetImportDecisions(_videoFiles, _series, new ParsedEpisodeInfo { Quality = expectedQuality }, true);
result.Single().LocalEpisode.Quality.Should().Be(expectedQuality);
}
@ -212,7 +206,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
GivenSpecifications(_pass1);
Mocker.GetMock<IParsingService>()
.Setup(c => c.GetLocalEpisode(It.IsAny<String>(), It.IsAny<Series>(), It.IsAny<Boolean>()))
.Setup(c => c.GetLocalEpisode(It.IsAny<String>(), It.IsAny<Series>(), It.IsAny<ParsedEpisodeInfo>(), It.IsAny<Boolean>()))
.Throws(new EpisodeNotFoundException("Episode not found"));
_videoFiles = new List<String>
@ -222,14 +216,130 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
"The.Office.S03E115.DVDRip.XviD-OSiTV"
};
Mocker.GetMock<IMediaFileService>()
.Setup(c => c.FilterExistingFiles(_videoFiles, It.IsAny<Series>()))
.Returns(_videoFiles);
GivenVideoFiles(_videoFiles);
Subject.GetImportDecisions(_videoFiles, _series);
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalEpisode(It.IsAny<String>(), It.IsAny<Series>(), It.IsAny<ParsedEpisodeInfo>(), It.IsAny<Boolean>()), Times.Exactly(_videoFiles.Count));
}
[Test]
public void should_not_use_folder_for_full_season()
{
var videoFiles = new[]
{
@"C:\Test\Unsorted\Series.Title.S01\S01E01.mkv".AsOsAgnostic(),
@"C:\Test\Unsorted\Series.Title.S01\S01E02.mkv".AsOsAgnostic(),
@"C:\Test\Unsorted\Series.Title.S01\S01E03.mkv".AsOsAgnostic()
};
GivenSpecifications(_pass1);
GivenVideoFiles(videoFiles);
var folderInfo = Parser.Parser.ParseTitle("Series.Title.S01");
Subject.GetImportDecisions(_videoFiles, _series, false);
Subject.GetImportDecisions(_videoFiles, _series, folderInfo, true);
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalEpisode(It.IsAny<string>(), It.IsAny<Series>(), null, true), Times.Exactly(3));
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalEpisode(It.IsAny<string>(), It.IsAny<Series>(), It.Is<ParsedEpisodeInfo>(p => p != null), true), Times.Never());
}
[Test]
public void should_not_use_folder_when_it_contains_more_than_one_valid_video_file()
{
var videoFiles = new[]
{
@"C:\Test\Unsorted\Series.Title.S01E01\S01E01.mkv".AsOsAgnostic(),
@"C:\Test\Unsorted\Series.Title.S01E01\1x01.mkv".AsOsAgnostic()
};
GivenSpecifications(_pass1);
GivenVideoFiles(videoFiles);
var folderInfo = Parser.Parser.ParseTitle("Series.Title.S01E01");
Subject.GetImportDecisions(_videoFiles, _series, folderInfo, true);
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalEpisode(It.IsAny<string>(), It.IsAny<Series>(), null, true), Times.Exactly(2));
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalEpisode(It.IsAny<string>(), It.IsAny<Series>(), It.Is<ParsedEpisodeInfo>(p => p != null), true), Times.Never());
}
[Test]
public void should_use_folder_when_only_one_video_file()
{
var videoFiles = new[]
{
@"C:\Test\Unsorted\Series.Title.S01E01\S01E01.mkv".AsOsAgnostic()
};
GivenSpecifications(_pass1);
GivenVideoFiles(videoFiles);
var folderInfo = Parser.Parser.ParseTitle("Series.Title.S01E01");
Subject.GetImportDecisions(_videoFiles, _series, folderInfo, true);
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalEpisode(It.IsAny<string>(), It.IsAny<Series>(), It.IsAny<ParsedEpisodeInfo>(), true), Times.Exactly(1));
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalEpisode(It.IsAny<string>(), It.IsAny<Series>(), null, true), Times.Never());
}
[Test]
public void should_use_folder_when_only_one_video_file_and_a_sample()
{
var videoFiles = new[]
{
@"C:\Test\Unsorted\Series.Title.S01E01\S01E01.mkv".AsOsAgnostic(),
@"C:\Test\Unsorted\Series.Title.S01E01\S01E01.sample.mkv".AsOsAgnostic()
};
GivenSpecifications(_pass1);
GivenVideoFiles(videoFiles.ToList());
Mocker.GetMock<IDetectSample>()
.Setup(s => s.IsSample(_series, It.IsAny<QualityModel>(), It.Is<string>(c => c.Contains("sample")), It.IsAny<long>(), It.IsAny<int>()))
.Returns(true);
var folderInfo = Parser.Parser.ParseTitle("Series.Title.S01E01");
Subject.GetImportDecisions(_videoFiles, _series, folderInfo, true);
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalEpisode(It.IsAny<string>(), It.IsAny<Series>(), It.IsAny<ParsedEpisodeInfo>(), true), Times.Exactly(2));
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalEpisode(It.IsAny<string>(), It.IsAny<Series>(), null, true), Times.Never());
}
[Test]
public void should_not_use_folder_name_if_file_name_is_scene_name()
{
var videoFiles = new[]
{
@"C:\Test\Unsorted\Series.Title.S01E01.720p.HDTV-LOL\Series.Title.S01E01.720p.HDTV-LOL.mkv".AsOsAgnostic()
};
GivenSpecifications(_pass1);
GivenVideoFiles(videoFiles);
var folderInfo = Parser.Parser.ParseTitle("Series.Title.S01E01.720p.HDTV-LOL");
Subject.GetImportDecisions(_videoFiles, _series, folderInfo, true);
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalEpisode(It.IsAny<string>(), It.IsAny<Series>(), null, true), Times.Exactly(1));
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalEpisode(It.IsAny<String>(), It.IsAny<Series>(), It.IsAny<Boolean>()), Times.Exactly(_videoFiles.Count));
.Verify(c => c.GetLocalEpisode(It.IsAny<string>(), It.IsAny<Series>(), It.Is<ParsedEpisodeInfo>(p => p != null), true), Times.Never());
}
}
}
}

@ -14,7 +14,7 @@ using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
{
[TestFixture]
public class SampleServiceFixture : CoreTest<SampleService>
public class SampleServiceFixture : CoreTest<DetectSample>
{
private Series _series;
private LocalEpisode _localEpisode;

@ -64,7 +64,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
GivenFileSize(100.Megabytes());
GivenFreeSpace(80.Megabytes());
Subject.IsSatisfiedBy(_localEpisode).Should().BeFalse();
Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeFalse();
ExceptionVerification.ExpectedWarns(1);
}
@ -74,7 +74,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
GivenFileSize(100.Megabytes());
GivenFreeSpace(150.Megabytes());
Subject.IsSatisfiedBy(_localEpisode).Should().BeFalse();
Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeFalse();
ExceptionVerification.ExpectedWarns(1);
}
@ -84,7 +84,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
GivenFileSize(100.Megabytes());
GivenFreeSpace(1.Gigabytes());
Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue();
Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeTrue();
}
[Test]
@ -93,7 +93,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
GivenFileSize(100.Megabytes());
GivenFreeSpace(1.Gigabytes());
Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue();
Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeTrue();
Mocker.GetMock<IDiskProvider>()
.Verify(v => v.GetAvailableSpace(_rootFolder), Times.Once());
@ -105,7 +105,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
GivenFileSize(100.Megabytes());
GivenFreeSpace(null);
Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue();
Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeTrue();
}
[Test]
@ -117,7 +117,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
.Setup(s => s.GetAvailableSpace(It.IsAny<String>()))
.Throws(new TestException());
Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue();
Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeTrue();
ExceptionVerification.ExpectedErrors(1);
}
@ -126,7 +126,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
{
_localEpisode.ExistingFile = true;
Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue();
Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeTrue();
Mocker.GetMock<IDiskProvider>()
.Verify(s => s.GetAvailableSpace(It.IsAny<String>()), Times.Never());
@ -141,7 +141,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
.Setup(s => s.GetAvailableSpace(It.IsAny<String>()))
.Returns(freeSpace);
Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue();
Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeTrue();
}
[Test]
@ -151,7 +151,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
.Setup(s => s.SkipFreeSpaceCheckWhenImporting)
.Returns(true);
Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue();
Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeTrue();
}
}
}

@ -34,13 +34,13 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
{
_localEpisode.ParsedEpisodeInfo.FullSeason = true;
Subject.IsSatisfiedBy(_localEpisode).Should().BeFalse();
Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeFalse();
}
[Test]
public void should_return_true_when_file_does_not_contain_the_full_season()
{
Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue();
Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeTrue();
}
}
}

@ -0,0 +1,84 @@
using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.MediaFiles.EpisodeImport.Specifications;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
{
[TestFixture]
public class MatchesFolderSpecificationFixture : CoreTest<MatchesFolderSpecification>
{
private LocalEpisode _localEpisode;
[SetUp]
public void Setup()
{
_localEpisode = Builder<LocalEpisode>.CreateNew()
.With(l => l.Path = @"C:\Test\Unsorted\Series.Title.S01E01.720p.HDTV-Sonarr\S01E05.mkv".AsOsAgnostic())
.With(l => l.ParsedEpisodeInfo =
Builder<ParsedEpisodeInfo>.CreateNew()
.With(p => p.EpisodeNumbers = new[] {5})
.With(p => p.FullSeason = false)
.Build())
.Build();
}
[Test]
public void should_be_accepted_for_existing_file()
{
_localEpisode.ExistingFile = true;
Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeTrue();
}
[Test]
public void should_be_accepted_if_folder_name_is_not_parseable()
{
_localEpisode.Path = @"C:\Test\Unsorted\Series.Title\S01E01.mkv".AsOsAgnostic();
Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeTrue();
}
[Test]
public void should_should_be_accepted_for_full_season()
{
_localEpisode.Path = @"C:\Test\Unsorted\Series.Title.S01\S01E01.mkv".AsOsAgnostic();
Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeTrue();
}
[Test]
public void should_be_accepted_if_file_and_folder_have_the_same_episode()
{
_localEpisode.ParsedEpisodeInfo.EpisodeNumbers = new[] { 1 };
_localEpisode.Path = @"C:\Test\Unsorted\Series.Title.S01E01.720p.HDTV-Sonarr\S01E01.mkv".AsOsAgnostic();
Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeTrue();
}
[Test]
public void should_be_accepted_if_file_is_one_episode_in_folder()
{
_localEpisode.ParsedEpisodeInfo.EpisodeNumbers = new[] { 1 };
_localEpisode.Path = @"C:\Test\Unsorted\Series.Title.S01E01E02.720p.HDTV-Sonarr\S01E01.mkv".AsOsAgnostic();
Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeTrue();
}
[Test]
public void should_be_rejected_if_file_and_folder_do_not_have_same_episode()
{
_localEpisode.Path = @"C:\Test\Unsorted\Series.Title.S01E01.720p.HDTV-Sonarr\S01E05.mkv".AsOsAgnostic();
Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeFalse();
}
[Test]
public void should_be_rejected_if_file_and_folder_do_not_have_same_episodes()
{
_localEpisode.ParsedEpisodeInfo.EpisodeNumbers = new[] { 5, 6 };
_localEpisode.Path = @"C:\Test\Unsorted\Series.Title.S01E01E02.720p.HDTV-Sonarr\S01E05E06.mkv".AsOsAgnostic();
Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeFalse();
}
}
}

@ -42,7 +42,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
public void should_return_true_for_existing_file()
{
_localEpisode.ExistingFile = true;
Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue();
Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeTrue();
}
}
}

@ -48,7 +48,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
[Test]
public void should_return_true_if_not_in_working_folder()
{
Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue();
Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeTrue();
}
[Test]
@ -59,7 +59,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
GivenInWorkingFolder();
GivenLastWriteTimeUtc(DateTime.UtcNow.AddHours(-1));
Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue();
Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeTrue();
}
[Test]
@ -68,7 +68,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
GivenInWorkingFolder();
GivenLastWriteTimeUtc(DateTime.UtcNow);
Subject.IsSatisfiedBy(_localEpisode).Should().BeFalse();
Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeFalse();
}
[Test]
@ -79,7 +79,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
GivenInWorkingFolder();
GivenLastWriteTimeUtc(DateTime.UtcNow.AddDays(-5));
Subject.IsSatisfiedBy(_localEpisode).Should().BeFalse();
Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeFalse();
}
}
}

@ -45,7 +45,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
.Build()
.ToList();
Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue();
Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeTrue();
}
[Test]
@ -58,7 +58,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
.Build()
.ToList();
Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue();
Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeTrue();
}
[Test]
@ -75,7 +75,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
.Build()
.ToList();
Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue();
Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeTrue();
}
[Test]
@ -92,7 +92,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
.Build()
.ToList();
Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue();
Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeTrue();
}
[Test]
@ -109,7 +109,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
.Build()
.ToList();
Subject.IsSatisfiedBy(_localEpisode).Should().BeFalse();
Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeFalse();
}
[Test]
@ -126,7 +126,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
.Build()
.ToList();
Subject.IsSatisfiedBy(_localEpisode).Should().BeFalse();
Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeFalse();
}
[Test]
@ -150,7 +150,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
.Build()
.ToList();
Subject.IsSatisfiedBy(_localEpisode).Should().BeFalse();
Subject.IsSatisfiedBy(_localEpisode).Accepted.Should().BeFalse();
}
}
}

@ -6,6 +6,7 @@ using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.EpisodeImport;
@ -44,9 +45,9 @@ namespace NzbDrone.Core.Test.MediaFiles
_rejectedDecisions.Add(new ImportDecision(new LocalEpisode(), "Rejected!"));
_rejectedDecisions.Add(new ImportDecision(new LocalEpisode(), "Rejected!"));
_rejectedDecisions.Add(new ImportDecision(new LocalEpisode(), "Rejected!"));
_rejectedDecisions.Add(new ImportDecision(new LocalEpisode(), new Rejection("Rejected!")));
_rejectedDecisions.Add(new ImportDecision(new LocalEpisode(), new Rejection("Rejected!")));
_rejectedDecisions.Add(new ImportDecision(new LocalEpisode(), new Rejection("Rejected!")));
foreach (var episode in episodes)
{

@ -227,6 +227,7 @@
<Compile Include="MediaFiles\EpisodeImport\SampleServiceFixture.cs" />
<Compile Include="MediaFiles\EpisodeImport\Specifications\FreeSpaceSpecificationFixture.cs" />
<Compile Include="MediaFiles\EpisodeImport\Specifications\FullSeasonSpecificationFixture.cs" />
<Compile Include="MediaFiles\EpisodeImport\Specifications\MatchesFolderSpecificationFixture.cs" />
<Compile Include="MediaFiles\EpisodeImport\Specifications\NotSampleSpecificationFixture.cs" />
<Compile Include="MediaFiles\EpisodeImport\Specifications\NotUnpackingSpecificationFixture.cs" />
<Compile Include="MediaFiles\EpisodeImport\Specifications\UpgradeSpecificationFixture.cs" />

@ -29,6 +29,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("08bbc153931ce3ca5fcafe1b92d3297285feb061.mkv")]
[TestCase("185d86a343e39f3341e35c4dad3ff159")]
[TestCase("ah63jka93jf0jh26ahjas961.mkv")]
[TestCase("qrdSD3rYzWb7cPdVIGSn4E7")]
public void should_not_parse_crap(string title)
{
Parser.Parser.ParseTitle(title).Should().BeNull();
@ -82,5 +83,11 @@ namespace NzbDrone.Core.Test.ParserTests
success.Should().Be(repetitions);
}
[TestCase("thebiggestloser1618finale")]
public void should_not_parse_file_name_without_proper_spacing(string fileName)
{
Parser.Parser.ParseTitle(fileName).Should().BeNull();
}
}
}

@ -5,10 +5,10 @@ using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.MetadataSource;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.TvTests
{

@ -4,11 +4,11 @@ using System.Linq;
using FizzWare.NBuilder;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.MetadataSource;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Tv.Commands;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.TvTests
{

@ -103,7 +103,7 @@ namespace NzbDrone.Core.MediaFiles
_logger.Trace("Finished getting episode files for: {0} [{1}]", series, videoFilesStopwatch.Elapsed);
var decisionsStopwatch = Stopwatch.StartNew();
var decisions = _importDecisionMaker.GetImportDecisions(mediaFileList, series, false);
var decisions = _importDecisionMaker.GetImportDecisions(mediaFileList, series);
decisionsStopwatch.Stop();
_logger.Trace("Import decisions complete for: {0} [{1}]", series, decisionsStopwatch.Elapsed);

@ -4,6 +4,7 @@ using System.IO;
using System.Linq;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.MediaFiles.EpisodeImport;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Tv;
@ -26,7 +27,7 @@ namespace NzbDrone.Core.MediaFiles
private readonly IParsingService _parsingService;
private readonly IMakeImportDecision _importDecisionMaker;
private readonly IImportApprovedEpisodes _importApprovedEpisodes;
private readonly ISampleService _sampleService;
private readonly IDetectSample _detectSample;
private readonly Logger _logger;
public DownloadedEpisodesImportService(IDiskProvider diskProvider,
@ -35,7 +36,7 @@ namespace NzbDrone.Core.MediaFiles
IParsingService parsingService,
IMakeImportDecision importDecisionMaker,
IImportApprovedEpisodes importApprovedEpisodes,
ISampleService sampleService,
IDetectSample detectSample,
Logger logger)
{
_diskProvider = diskProvider;
@ -44,7 +45,7 @@ namespace NzbDrone.Core.MediaFiles
_parsingService = parsingService;
_importDecisionMaker = importDecisionMaker;
_importApprovedEpisodes = importApprovedEpisodes;
_sampleService = sampleService;
_detectSample = detectSample;
_logger = logger;
}
@ -115,9 +116,12 @@ namespace NzbDrone.Core.MediaFiles
}
var cleanedUpName = GetCleanedUpFolderName(directoryInfo.Name);
var quality = QualityParser.ParseQuality(cleanedUpName);
var folderInfo = Parser.Parser.ParseTitle(directoryInfo.Name);
_logger.Debug("{0} folder quality: {1}", cleanedUpName, quality);
if (folderInfo != null)
{
_logger.Debug("{0} folder quality: {1}", cleanedUpName, folderInfo.Quality);
}
var videoFiles = _diskScanService.GetVideoFiles(directoryInfo.FullName);
@ -135,7 +139,7 @@ namespace NzbDrone.Core.MediaFiles
}
}
var decisions = _importDecisionMaker.GetImportDecisions(videoFiles.ToList(), series, true, quality);
var decisions = _importDecisionMaker.GetImportDecisions(videoFiles.ToList(), series, folderInfo, true);
var importResults = _importApprovedEpisodes.Import(decisions, true, downloadClientItem);
if ((downloadClientItem == null || !downloadClientItem.IsReadOnly) && importResults.Any() && ShouldDeleteFolder(directoryInfo, series))
@ -177,7 +181,9 @@ namespace NzbDrone.Core.MediaFiles
}
}
var decisions = _importDecisionMaker.GetImportDecisions(new List<string>() { fileInfo.FullName }, series, true);
var folderInfo = Parser.Parser.ParseTitle(fileInfo.DirectoryName);
var decisions = _importDecisionMaker.GetImportDecisions(new List<string>() { fileInfo.FullName }, series, folderInfo, true);
return _importApprovedEpisodes.Import(decisions, true, downloadClientItem);
}
@ -207,7 +213,7 @@ namespace NzbDrone.Core.MediaFiles
var size = _diskProvider.GetFileSize(videoFile);
var quality = QualityParser.ParseQuality(videoFile);
if (!_sampleService.IsSample(series, quality, videoFile, size,
if (!_detectSample.IsSample(series, quality, videoFile, size,
episodeParseResult.SeasonNumber))
{
_logger.Warn("Non-sample file detected: [{0}]", videoFile);
@ -227,14 +233,14 @@ namespace NzbDrone.Core.MediaFiles
private ImportResult FileIsLockedResult(string videoFile)
{
_logger.Debug("[{0}] is currently locked by another process, skipping", videoFile);
return new ImportResult(new ImportDecision(new LocalEpisode { Path = videoFile }, "Locked file, try again later"), "Locked file, try again later");
return new ImportResult(new ImportDecision(new LocalEpisode { Path = videoFile }, new Rejection("Locked file, try again later")), "Locked file, try again later");
}
private ImportResult UnknownSeriesResult(string message, string videoFile = null)
{
var localEpisode = videoFile == null ? null : new LocalEpisode { Path = videoFile };
return new ImportResult(new ImportDecision(localEpisode, "Unknown Series"), message);
return new ImportResult(new ImportDecision(localEpisode, new Rejection("Unknown Series")), message);
}
}
}

@ -8,19 +8,19 @@ using NzbDrone.Core.Tv;
namespace NzbDrone.Core.MediaFiles.EpisodeImport
{
public interface ISampleService
public interface IDetectSample
{
bool IsSample(Series series, QualityModel quality, string path, long size, int seasonNumber);
}
public class SampleService : ISampleService
public class DetectSample : IDetectSample
{
private readonly IVideoFileInfoReader _videoFileInfoReader;
private readonly Logger _logger;
private static List<Quality> _largeSampleSizeQualities = new List<Quality> { Quality.HDTV1080p, Quality.WEBDL1080p, Quality.Bluray1080p };
public SampleService(IVideoFileInfoReader videoFileInfoReader, Logger logger)
public DetectSample(IVideoFileInfoReader videoFileInfoReader, Logger logger)
{
_videoFileInfoReader = videoFileInfoReader;
_logger = logger;

@ -3,8 +3,8 @@ using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.MediaFiles.EpisodeImport
{
public interface IImportDecisionEngineSpecification : IRejectWithReason
public interface IImportDecisionEngineSpecification
{
bool IsSatisfiedBy(LocalEpisode localEpisode);
Decision IsSatisfiedBy(LocalEpisode localEpisode);
}
}

@ -120,7 +120,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
//Adding all the rejected decisions
importResults.AddRange(decisions.Where(c => !c.Approved)
.Select(d => new ImportResult(d, d.Rejections.ToArray())));
.Select(d => new ImportResult(d, d.Rejections.Select(r => r.Reason).ToArray())));
return importResults;
}

@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.MediaFiles.EpisodeImport
@ -8,7 +9,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
public class ImportDecision
{
public LocalEpisode LocalEpisode { get; private set; }
public IEnumerable<string> Rejections { get; private set; }
public IEnumerable<Rejection> Rejections { get; private set; }
public bool Approved
{
@ -18,7 +19,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
}
}
public ImportDecision(LocalEpisode localEpisode, params string[] rejections)
public ImportDecision(LocalEpisode localEpisode, params Rejection[] rejections)
{
LocalEpisode = localEpisode;
Rejections = rejections.ToList();

@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
@ -15,23 +17,26 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
{
public interface IMakeImportDecision
{
List<ImportDecision> GetImportDecisions(List<String> videoFiles, Series series, bool sceneSource, QualityModel quality = null);
List<ImportDecision> GetImportDecisions(List<String> videoFiles, Series series);
List<ImportDecision> GetImportDecisions(List<string> videoFiles, Series series, ParsedEpisodeInfo folderInfo, bool sceneSource);
}
public class ImportDecisionMaker : IMakeImportDecision
{
private readonly IEnumerable<IRejectWithReason> _specifications;
private readonly IEnumerable<IImportDecisionEngineSpecification> _specifications;
private readonly IParsingService _parsingService;
private readonly IMediaFileService _mediaFileService;
private readonly IDiskProvider _diskProvider;
private readonly IVideoFileInfoReader _videoFileInfoReader;
private readonly IDetectSample _detectSample;
private readonly Logger _logger;
public ImportDecisionMaker(IEnumerable<IRejectWithReason> specifications,
public ImportDecisionMaker(IEnumerable<IImportDecisionEngineSpecification> specifications,
IParsingService parsingService,
IMediaFileService mediaFileService,
IDiskProvider diskProvider,
IVideoFileInfoReader videoFileInfoReader,
IDetectSample detectSample,
Logger logger)
{
_specifications = specifications;
@ -39,98 +44,96 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
_mediaFileService = mediaFileService;
_diskProvider = diskProvider;
_videoFileInfoReader = videoFileInfoReader;
_detectSample = detectSample;
_logger = logger;
}
public List<ImportDecision> GetImportDecisions(List<string> videoFiles, Series series, bool sceneSource, QualityModel quality = null)
public List<ImportDecision> GetImportDecisions(List<string> videoFiles, Series series)
{
return GetImportDecisions(videoFiles, series, null, false);
}
public List<ImportDecision> GetImportDecisions(List<string> videoFiles, Series series, ParsedEpisodeInfo folderInfo, bool sceneSource)
{
var newFiles = _mediaFileService.FilterExistingFiles(videoFiles.ToList(), series);
_logger.Debug("Analyzing {0}/{1} files.", newFiles.Count, videoFiles.Count());
return GetDecisions(newFiles, series, sceneSource, quality).ToList();
var shouldUseFolderName = ShouldUseFolderName(videoFiles, series, folderInfo);
var decisions = new List<ImportDecision>();
foreach (var file in newFiles)
{
decisions.AddIfNotNull(GetDecision(file, series, folderInfo, sceneSource, shouldUseFolderName));
}
return decisions;
}
private IEnumerable<ImportDecision> GetDecisions(IEnumerable<String> videoFiles, Series series, bool sceneSource, QualityModel quality = null)
private ImportDecision GetDecision(string file, Series series, ParsedEpisodeInfo folderInfo, bool sceneSource, bool shouldUseFolderName)
{
foreach (var file in videoFiles)
ImportDecision decision = null;
try
{
ImportDecision decision = null;
var localEpisode = _parsingService.GetLocalEpisode(file, series, shouldUseFolderName ? folderInfo : null, sceneSource);
try
if (localEpisode != null)
{
var localEpisode = _parsingService.GetLocalEpisode(file, series, sceneSource);
localEpisode.Quality = GetQuality(folderInfo, localEpisode.Quality, series);
localEpisode.Size = _diskProvider.GetFileSize(file);
if (localEpisode != null)
{
if (quality != null &&
new QualityModelComparer(localEpisode.Series.Profile).Compare(quality,
localEpisode.Quality) > 0)
{
_logger.Debug("Using quality from folder: {0}", quality);
localEpisode.Quality = quality;
}
localEpisode.Size = _diskProvider.GetFileSize(file);
_logger.Debug("Size: {0}", localEpisode.Size);
//TODO: make it so media info doesn't ruin the import process of a new series
if (sceneSource)
{
localEpisode.MediaInfo = _videoFileInfoReader.GetMediaInfo(file);
}
decision = GetDecision(localEpisode);
}
_logger.Debug("Size: {0}", localEpisode.Size);
else
//TODO: make it so media info doesn't ruin the import process of a new series
if (sceneSource)
{
localEpisode = new LocalEpisode();
localEpisode.Path = file;
decision = new ImportDecision(localEpisode, "Unable to parse file");
localEpisode.MediaInfo = _videoFileInfoReader.GetMediaInfo(file);
}
decision = GetDecision(localEpisode);
}
catch (EpisodeNotFoundException e)
else
{
var localEpisode = new LocalEpisode();
localEpisode = new LocalEpisode();
localEpisode.Path = file;
decision = new ImportDecision(localEpisode, e.Message);
}
catch (Exception e)
{
_logger.ErrorException("Couldn't import file. " + file, e);
decision = new ImportDecision(localEpisode, new Rejection("Unable to parse file"));
}
}
catch (EpisodeNotFoundException e)
{
var localEpisode = new LocalEpisode();
localEpisode.Path = file;
if (decision != null)
{
yield return decision;
}
decision = new ImportDecision(localEpisode, new Rejection(e.Message));
}
catch (Exception e)
{
_logger.ErrorException("Couldn't import file. " + file, e);
}
return decision;
}
private ImportDecision GetDecision(LocalEpisode localEpisode)
{
var reasons = _specifications.Select(c => EvaluateSpec(c, localEpisode))
.Where(c => !string.IsNullOrWhiteSpace(c));
.Where(c => c != null);
return new ImportDecision(localEpisode, reasons.ToArray());
}
private string EvaluateSpec(IRejectWithReason spec, LocalEpisode localEpisode)
private Rejection EvaluateSpec(IImportDecisionEngineSpecification spec, LocalEpisode localEpisode)
{
try
{
if (string.IsNullOrWhiteSpace(spec.RejectionReason))
{
throw new InvalidOperationException("[Need Rejection Text]");
}
var result = spec.IsSatisfiedBy(localEpisode);
var generalSpecification = spec as IImportDecisionEngineSpecification;
if (generalSpecification != null && !generalSpecification.IsSatisfiedBy(localEpisode))
if (!result.Accepted)
{
return spec.RejectionReason;
return new Rejection(result.Reason);
}
}
catch (Exception e)
@ -138,10 +141,55 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
//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 new Rejection(String.Format("{0}: {1}", spec.GetType().Name, e.Message));
}
return null;
}
private bool ShouldUseFolderName(List<string> videoFiles, Series series, ParsedEpisodeInfo folderInfo)
{
if (folderInfo == null)
{
return false;
}
if (folderInfo.FullSeason)
{
return false;
}
return videoFiles.Count(file =>
{
var size = _diskProvider.GetFileSize(file);
var fileQuality = QualityParser.ParseQuality(file);
var sample = _detectSample.IsSample(series, GetQuality(folderInfo, fileQuality, series), file, size, folderInfo.SeasonNumber);
if (sample)
{
return false;
}
if (SceneChecker.IsSceneTitle(Path.GetFileName(file)))
{
return false;
}
return true;
}) == 1;
}
private QualityModel GetQuality(ParsedEpisodeInfo folderInfo, QualityModel fileQuality, Series series)
{
if (folderInfo != null &&
new QualityModelComparer(series.Profile).Compare(folderInfo.Quality,
fileQuality) > 0)
{
_logger.Debug("Using quality from folder: {0}", folderInfo.Quality);
return folderInfo.Quality;
}
return fileQuality;
}
}
}

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.EnsureThat;
@ -8,7 +7,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
public class ImportResult
{
public ImportDecision ImportDecision { get; private set; }
public List<String> Errors { get; private set; }
public List<string> Errors { get; private set; }
public ImportResultType Result
{
@ -28,7 +27,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
}
}
public ImportResult(ImportDecision importDecision, params String[] errors)
public ImportResult(ImportDecision importDecision, params string[] errors)
{
Ensure.That(importDecision, () => importDecision).IsNotNull();

@ -3,6 +3,7 @@ using System.IO;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
@ -20,14 +21,12 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
_logger = logger;
}
public string RejectionReason { get { return "Not enough free space"; } }
public bool IsSatisfiedBy(LocalEpisode localEpisode)
public Decision IsSatisfiedBy(LocalEpisode localEpisode)
{
if (_configService.SkipFreeSpaceCheckWhenImporting)
{
_logger.Debug("Skipping free space check when importing");
return true;
return Decision.Accept();
}
try
@ -35,7 +34,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
if (localEpisode.ExistingFile)
{
_logger.Debug("Skipping free space check for existing episode");
return true;
return Decision.Accept();
}
var path = Directory.GetParent(localEpisode.Series.Path);
@ -44,13 +43,13 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
if (!freeSpace.HasValue)
{
_logger.Debug("Free space check returned an invalid result for: {0}", path);
return true;
return Decision.Accept();
}
if (freeSpace < localEpisode.Size + 100.Megabytes())
{
_logger.Warn("Not enough free space ({0}) to import: {1} ({2})", freeSpace, localEpisode, localEpisode.Size);
return false;
return Decision.Reject("Not enough free space");
}
}
catch (DirectoryNotFoundException ex)
@ -62,7 +61,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
_logger.ErrorException("Unable to check free disk space while importing: " + localEpisode.Path, ex);
}
return true;
return Decision.Accept();
}
}
}

@ -1,4 +1,5 @@
using NLog;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
@ -12,17 +13,15 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
_logger = logger;
}
public string RejectionReason { get { return "Full season file"; } }
public bool IsSatisfiedBy(LocalEpisode localEpisode)
public Decision IsSatisfiedBy(LocalEpisode localEpisode)
{
if (localEpisode.ParsedEpisodeInfo.FullSeason)
{
_logger.Debug("Single episode file detected as containing all episodes in the season");
return false;
return Decision.Reject("Single episode file contains all episodes in seasons");
}
return true;
return Decision.Accept();
}
}
}

@ -0,0 +1,54 @@
using System;
using System.IO;
using System.Linq;
using NLog;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
{
public class MatchesFolderSpecification : IImportDecisionEngineSpecification
{
private readonly Logger _logger;
public MatchesFolderSpecification(Logger logger)
{
_logger = logger;
}
public Decision IsSatisfiedBy(LocalEpisode localEpisode)
{
if (localEpisode.ExistingFile)
{
return Decision.Accept();
}
var folderInfo = Parser.Parser.ParseTitle(new FileInfo(localEpisode.Path).DirectoryName);
if (folderInfo == null)
{
return Decision.Accept();
}
if (folderInfo.FullSeason)
{
return Decision.Accept();
}
var unexpected = localEpisode.ParsedEpisodeInfo.EpisodeNumbers.Where(f => !folderInfo.EpisodeNumbers.Contains(f)).ToList();
if (unexpected.Any())
{
_logger.Debug("Unexpected episode number(s) in file: {0}", unexpected);
if (unexpected.Count == 1)
{
return Decision.Reject("Episode Number {0} was unexpected", unexpected.First());
}
return Decision.Reject("Episode Numbers {0} were unexpected", String.Join(", ", unexpected));
}
return Decision.Accept();
}
}
}

@ -1,35 +1,41 @@
using NLog;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
{
public class NotSampleSpecification : IImportDecisionEngineSpecification
{
private readonly ISampleService _sampleService;
private readonly IDetectSample _detectSample;
private readonly Logger _logger;
public NotSampleSpecification(ISampleService sampleService,
public NotSampleSpecification(IDetectSample detectSample,
Logger logger)
{
_sampleService = sampleService;
_detectSample = detectSample;
_logger = logger;
}
public string RejectionReason { get { return "Sample"; } }
public bool IsSatisfiedBy(LocalEpisode localEpisode)
public Decision IsSatisfiedBy(LocalEpisode localEpisode)
{
if (localEpisode.ExistingFile)
{
_logger.Debug("Existing file, skipping sample check");
return true;
return Decision.Accept();
}
var sample = _detectSample.IsSample(localEpisode.Series,
localEpisode.Quality,
localEpisode.Path,
localEpisode.Size,
localEpisode.SeasonNumber);
if (sample)
{
return Decision.Reject("Sample");
}
return !_sampleService.IsSample(localEpisode.Series,
localEpisode.Quality,
localEpisode.Path,
localEpisode.Size,
localEpisode.SeasonNumber);
return Decision.Accept();
}
}
}

@ -4,6 +4,7 @@ using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
@ -21,14 +22,12 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
_logger = logger;
}
public string RejectionReason { get { return "File is still being unpacked"; } }
public bool IsSatisfiedBy(LocalEpisode localEpisode)
public Decision IsSatisfiedBy(LocalEpisode localEpisode)
{
if (localEpisode.ExistingFile)
{
_logger.Debug("{0} is in series folder, unpacking check", localEpisode.Path);
return true;
return Decision.Accept();
}
foreach (var workingFolder in _configService.DownloadClientWorkingFolders.Split('|'))
@ -41,13 +40,13 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
if (OsInfo.IsNotWindows)
{
_logger.Debug("{0} is still being unpacked", localEpisode.Path);
return false;
return Decision.Reject("File is still being unpacked");
}
if (_diskProvider.FileGetLastWrite(localEpisode.Path) > DateTime.UtcNow.AddMinutes(-5))
{
_logger.Debug("{0} appears to be unpacking still", localEpisode.Path);
return false;
return Decision.Reject("File is still being unpacked");
}
}
@ -55,7 +54,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
}
}
return true;
return Decision.Accept();
}
}
}

@ -1,5 +1,6 @@
using System.Linq;
using NLog;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Qualities;
@ -14,18 +15,16 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
_logger = logger;
}
public string RejectionReason { get { return "Not an upgrade for existing episode file(s)"; } }
public bool IsSatisfiedBy(LocalEpisode localEpisode)
public Decision IsSatisfiedBy(LocalEpisode localEpisode)
{
var qualityComparer = new QualityModelComparer(localEpisode.Series.Profile);
if (localEpisode.Episodes.Any(e => e.EpisodeFileId != 0 && qualityComparer.Compare(e.EpisodeFile.Value.Quality, localEpisode.Quality) > 0))
{
_logger.Debug("This file isn't an upgrade for all episodes. Skipping {0}", localEpisode.Path);
return false;
return Decision.Reject("Not an upgrade for existing episode file(s)");
}
return true;
return Decision.Accept();
}
}
}

@ -61,7 +61,7 @@ namespace NzbDrone.Core.Metadata
{
try
{
var localEpisode = _parsingService.GetLocalEpisode(possibleMetadataFile, message.Series, false);
var localEpisode = _parsingService.GetLocalEpisode(possibleMetadataFile, message.Series);
if (localEpisode == null)
{

@ -561,8 +561,9 @@
<Compile Include="MediaFiles\EpisodeImport\ImportDecisionMaker.cs" />
<Compile Include="MediaFiles\EpisodeImport\ImportResultType.cs" />
<Compile Include="MediaFiles\EpisodeImport\ManualImportService.cs" />
<Compile Include="MediaFiles\EpisodeImport\SampleService.cs" />
<Compile Include="MediaFiles\EpisodeImport\DetectSample.cs" />
<Compile Include="MediaFiles\EpisodeImport\Specifications\FreeSpaceSpecification.cs" />
<Compile Include="MediaFiles\EpisodeImport\Specifications\MatchesFolderSpecification.cs" />
<Compile Include="MediaFiles\EpisodeImport\Specifications\FullSeasonSpecification.cs" />
<Compile Include="MediaFiles\EpisodeImport\Specifications\NotSampleSpecification.cs" />
<Compile Include="MediaFiles\EpisodeImport\Specifications\NotUnpackingSpecification.cs" />

@ -104,7 +104,7 @@ namespace NzbDrone.Core.Parser
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Episodes with single digit episode number (S01E1, S01E5E6, etc)
new Regex(@"^(?<title>.*?)(?:\W?S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]){1,2}(?<episode>\d{1}))+)+(\W+|_|$)(?!\\)",
new Regex(@"^(?<title>.*?)(?:(?:_|-|\s|\.)S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]){1,2}(?<episode>\d{1}))+)+(\W+|_|$)(?!\\)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Anime - Title Absolute Episode Number (e66)

@ -13,7 +13,8 @@ namespace NzbDrone.Core.Parser
{
public interface IParsingService
{
LocalEpisode GetLocalEpisode(string filename, Series series, bool sceneSource);
LocalEpisode GetLocalEpisode(string filename, Series series);
LocalEpisode GetLocalEpisode(string filename, Series series, ParsedEpisodeInfo folderInfo, bool sceneSource);
Series GetSeries(string title);
RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, Int32 tvRageId = 0, SearchCriteriaBase searchCriteria = null);
RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, Int32 seriesId, IEnumerable<Int32> episodeIds);
@ -39,9 +40,25 @@ namespace NzbDrone.Core.Parser
_logger = logger;
}
public LocalEpisode GetLocalEpisode(string filename, Series series, bool sceneSource)
public LocalEpisode GetLocalEpisode(string filename, Series series)
{
var parsedEpisodeInfo = Parser.ParsePath(filename);
return GetLocalEpisode(filename, series, null, false);
}
public LocalEpisode GetLocalEpisode(string filename, Series series, ParsedEpisodeInfo folderInfo, bool sceneSource)
{
ParsedEpisodeInfo parsedEpisodeInfo;
if (folderInfo != null)
{
parsedEpisodeInfo = folderInfo.JsonClone();
parsedEpisodeInfo.Quality = QualityParser.ParseQuality(Path.GetFileName(filename));
}
else
{
parsedEpisodeInfo = Parser.ParsePath(filename);
}
if (parsedEpisodeInfo == null || parsedEpisodeInfo.IsPossibleSpecialEpisode)
{

@ -1,4 +1,6 @@
namespace NzbDrone.Core.Parser
using System;
namespace NzbDrone.Core.Parser
{
public static class SceneChecker
{
@ -10,10 +12,14 @@
if (title.Contains(" ")) return false;
var parsedTitle = Parser.ParseTitle(title);
if (parsedTitle == null
|| parsedTitle.ReleaseGroup == null
|| parsedTitle.Quality.Quality == Qualities.Quality.Unknown
|| string.IsNullOrWhiteSpace(parsedTitle.SeriesTitle)) return false;
if (parsedTitle == null ||
parsedTitle.ReleaseGroup == null ||
parsedTitle.Quality.Quality == Qualities.Quality.Unknown ||
String.IsNullOrWhiteSpace(parsedTitle.SeriesTitle))
{
return false;
}
return true;
}

@ -91,7 +91,6 @@
<Compile Include="LoggingTest.cs" />
<Compile Include="MockerExtensions.cs" />
<Compile Include="NzbDroneRunner.cs" />
<Compile Include="ObjectExtensions.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ReflectionExtensions.cs" />
<Compile Include="StringExtensions.cs" />

Loading…
Cancel
Save