Merge branch 'develop'
This commit is contained in:
commit
878a89b4e2
|
@ -127,6 +127,9 @@ Function PackageOsx()
|
||||||
Write-Host "Adding sqlite dylibs"
|
Write-Host "Adding sqlite dylibs"
|
||||||
Copy-Item "$sourceFolder\Libraries\sqlite\*.dylib" "$outputFolderOsx"
|
Copy-Item "$sourceFolder\Libraries\sqlite\*.dylib" "$outputFolderOsx"
|
||||||
|
|
||||||
|
Write-Host "Adding MediaInfo dylib"
|
||||||
|
Copy-Item "$sourceFolder\Libraries\MediaInfo\*.dylib" "$outputFolderOsx"
|
||||||
|
|
||||||
Write-Host "##teamcity[progressFinish 'Creating OS X Package']"
|
Write-Host "##teamcity[progressFinish 'Creating OS X Package']"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -184,6 +184,9 @@ namespace Exceptron.Client
|
||||||
{
|
{
|
||||||
report.cul = Thread.CurrentThread.CurrentCulture.Name;
|
report.cul = Thread.CurrentThread.CurrentCulture.Name;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(report.cul))
|
||||||
|
report.cul = "en";
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
report.os = Environment.OSVersion.VersionString;
|
report.os = Environment.OSVersion.VersionString;
|
||||||
|
|
Binary file not shown.
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<configuration>
|
<configuration>
|
||||||
<dllmap os="osx" dll="MediaInfo.dll" target="libmediainfo.dylib"/>
|
<dllmap os="osx" dll="MediaInfo.dll" target="libmediainfo.0.dylib"/>
|
||||||
<dllmap os="linux" dll="MediaInfo.dll" target="libmediainfo.so.0" />
|
<dllmap os="linux" dll="MediaInfo.dll" target="libmediainfo.so.0" />
|
||||||
<dllmap os="freebsd" dll="MediaInfo.dll" target="libmediainfo.so.0" />
|
<dllmap os="freebsd" dll="MediaInfo.dll" target="libmediainfo.so.0" />
|
||||||
<dllmap os="solaris" dll="MediaInfo.dll" target="libmediainfo.so.0.0.0" />
|
<dllmap os="solaris" dll="MediaInfo.dll" target="libmediainfo.so.0.0.0" />
|
||||||
|
|
|
@ -12,12 +12,10 @@ namespace NzbDrone.Api.Authentication
|
||||||
{
|
{
|
||||||
public class EnableStatelessAuthInNancy : IRegisterNancyPipeline
|
public class EnableStatelessAuthInNancy : IRegisterNancyPipeline
|
||||||
{
|
{
|
||||||
private readonly IAuthenticationService _authenticationService;
|
|
||||||
private static String API_KEY;
|
private static String API_KEY;
|
||||||
|
|
||||||
public EnableStatelessAuthInNancy(IAuthenticationService authenticationService, IConfigFileProvider configFileProvider)
|
public EnableStatelessAuthInNancy(IConfigFileProvider configFileProvider)
|
||||||
{
|
{
|
||||||
_authenticationService = authenticationService;
|
|
||||||
API_KEY = configFileProvider.ApiKey;
|
API_KEY = configFileProvider.ApiKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,16 +28,11 @@ namespace NzbDrone.Api.Authentication
|
||||||
{
|
{
|
||||||
Response response = null;
|
Response response = null;
|
||||||
|
|
||||||
if (!RuntimeInfo.IsProduction && context.Request.IsLocalRequest())
|
|
||||||
{
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
var authorizationHeader = context.Request.Headers.Authorization;
|
var authorizationHeader = context.Request.Headers.Authorization;
|
||||||
var apiKeyHeader = context.Request.Headers["X-Api-Key"].FirstOrDefault();
|
var apiKeyHeader = context.Request.Headers["X-Api-Key"].FirstOrDefault();
|
||||||
var apiKey = apiKeyHeader.IsNullOrWhiteSpace() ? authorizationHeader : apiKeyHeader;
|
var apiKey = apiKeyHeader.IsNullOrWhiteSpace() ? authorizationHeader : apiKeyHeader;
|
||||||
|
|
||||||
if (context.Request.IsApiRequest() && !ValidApiKey(apiKey) && !IsAuthenticated(context))
|
if (context.Request.IsApiRequest() && !ValidApiKey(apiKey))
|
||||||
{
|
{
|
||||||
response = new Response { StatusCode = HttpStatusCode.Unauthorized };
|
response = new Response { StatusCode = HttpStatusCode.Unauthorized };
|
||||||
}
|
}
|
||||||
|
@ -49,15 +42,9 @@ namespace NzbDrone.Api.Authentication
|
||||||
|
|
||||||
private bool ValidApiKey(string apiKey)
|
private bool ValidApiKey(string apiKey)
|
||||||
{
|
{
|
||||||
if (apiKey.IsNullOrWhiteSpace()) return false;
|
if (!API_KEY.Equals(apiKey)) return false;
|
||||||
if (!apiKey.Equals(API_KEY)) return false;
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsAuthenticated(NancyContext context)
|
|
||||||
{
|
|
||||||
return _authenticationService.Enabled && _authenticationService.IsAuthenticated(context);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -40,8 +40,8 @@ namespace NzbDrone.Api.Calendar
|
||||||
var occurrence = icalCalendar.Create<Event>();
|
var occurrence = icalCalendar.Create<Event>();
|
||||||
occurrence.UID = "NzbDrone_episode_" + episode.Id.ToString();
|
occurrence.UID = "NzbDrone_episode_" + episode.Id.ToString();
|
||||||
occurrence.Status = episode.HasFile ? EventStatus.Confirmed : EventStatus.Tentative;
|
occurrence.Status = episode.HasFile ? EventStatus.Confirmed : EventStatus.Tentative;
|
||||||
occurrence.Start = new iCalDateTime(episode.AirDateUtc.Value);
|
occurrence.Start = new iCalDateTime(episode.AirDateUtc.Value) { HasTime = true };
|
||||||
occurrence.End = new iCalDateTime(episode.AirDateUtc.Value.AddMinutes(episode.Series.Runtime));
|
occurrence.End = new iCalDateTime(episode.AirDateUtc.Value.AddMinutes(episode.Series.Runtime)) { HasTime = true };
|
||||||
occurrence.Description = episode.Overview;
|
occurrence.Description = episode.Overview;
|
||||||
occurrence.Categories = new List<string>() { episode.Series.Network };
|
occurrence.Categories = new List<string>() { episode.Series.Network };
|
||||||
|
|
||||||
|
|
|
@ -17,5 +17,7 @@ namespace NzbDrone.Api.Config
|
||||||
public String FolderChmod { get; set; }
|
public String FolderChmod { get; set; }
|
||||||
public String ChownUser { get; set; }
|
public String ChownUser { get; set; }
|
||||||
public String ChownGroup { get; set; }
|
public String ChownGroup { get; set; }
|
||||||
|
|
||||||
|
public Boolean SkipFreeSpaceCheckWhenImporting { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ namespace NzbDrone.Api.Extensions
|
||||||
{
|
{
|
||||||
public static bool IsApiRequest(this Request request)
|
public static bool IsApiRequest(this Request request)
|
||||||
{
|
{
|
||||||
return request.Path.StartsWith("/api/", StringComparison.InvariantCultureIgnoreCase) || request.IsLogFileRequest();
|
return request.Path.StartsWith("/api/", StringComparison.InvariantCultureIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool IsSignalRRequest(this Request request)
|
public static bool IsSignalRRequest(this Request request)
|
||||||
|
@ -21,11 +21,5 @@ namespace NzbDrone.Api.Extensions
|
||||||
request.UserHostAddress.Equals("127.0.0.1") ||
|
request.UserHostAddress.Equals("127.0.0.1") ||
|
||||||
request.UserHostAddress.Equals("::1"));
|
request.UserHostAddress.Equals("::1"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsLogFileRequest(this Request request)
|
|
||||||
{
|
|
||||||
return request.Path.StartsWith("/log/", StringComparison.InvariantCultureIgnoreCase) &&
|
|
||||||
request.Path.EndsWith(".txt", StringComparison.InvariantCultureIgnoreCase);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,11 +4,15 @@ using System.Linq;
|
||||||
using NzbDrone.Common;
|
using NzbDrone.Common;
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
|
using Nancy;
|
||||||
|
using Nancy.Responses;
|
||||||
|
|
||||||
namespace NzbDrone.Api.Logs
|
namespace NzbDrone.Api.Logs
|
||||||
{
|
{
|
||||||
public class LogFileModule : NzbDroneRestModule<LogFileResource>
|
public class LogFileModule : NzbDroneRestModule<LogFileResource>
|
||||||
{
|
{
|
||||||
|
private const string LOGFILE_ROUTE = @"/(?<filename>nzbdrone(?:\.\d+)?\.txt)";
|
||||||
|
|
||||||
private readonly IAppFolderInfo _appFolderInfo;
|
private readonly IAppFolderInfo _appFolderInfo;
|
||||||
private readonly IDiskProvider _diskProvider;
|
private readonly IDiskProvider _diskProvider;
|
||||||
|
|
||||||
|
@ -19,6 +23,8 @@ namespace NzbDrone.Api.Logs
|
||||||
_appFolderInfo = appFolderInfo;
|
_appFolderInfo = appFolderInfo;
|
||||||
_diskProvider = diskProvider;
|
_diskProvider = diskProvider;
|
||||||
GetResourceAll = GetLogFiles;
|
GetResourceAll = GetLogFiles;
|
||||||
|
|
||||||
|
Get[LOGFILE_ROUTE] = options => GetLogFile(options.filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<LogFileResource> GetLogFiles()
|
private List<LogFileResource> GetLogFiles()
|
||||||
|
@ -41,5 +47,17 @@ namespace NzbDrone.Api.Logs
|
||||||
|
|
||||||
return result.OrderByDescending(l => l.LastWriteTime).ToList();
|
return result.OrderByDescending(l => l.LastWriteTime).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Response GetLogFile(string filename)
|
||||||
|
{
|
||||||
|
var filePath = Path.Combine(_appFolderInfo.GetLogFolder(), filename);
|
||||||
|
|
||||||
|
if (!_diskProvider.FileExists(filePath))
|
||||||
|
return new NotFoundResponse();
|
||||||
|
|
||||||
|
var data = _diskProvider.ReadAllText(filePath);
|
||||||
|
|
||||||
|
return new TextResponse(data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -32,7 +32,13 @@ namespace NzbDrone.Api.Series
|
||||||
public SeriesModule(ICommandExecutor commandExecutor,
|
public SeriesModule(ICommandExecutor commandExecutor,
|
||||||
ISeriesService seriesService,
|
ISeriesService seriesService,
|
||||||
ISeriesStatisticsService seriesStatisticsService,
|
ISeriesStatisticsService seriesStatisticsService,
|
||||||
IMapCoversToLocal coverMapper)
|
IMapCoversToLocal coverMapper,
|
||||||
|
RootFolderValidator rootFolderValidator,
|
||||||
|
PathExistsValidator pathExistsValidator,
|
||||||
|
SeriesPathValidator seriesPathValidator,
|
||||||
|
SeriesExistsValidator seriesExistsValidator,
|
||||||
|
DroneFactoryValidator droneFactoryValidator
|
||||||
|
)
|
||||||
: base(commandExecutor)
|
: base(commandExecutor)
|
||||||
{
|
{
|
||||||
_commandExecutor = commandExecutor;
|
_commandExecutor = commandExecutor;
|
||||||
|
@ -48,11 +54,18 @@ namespace NzbDrone.Api.Series
|
||||||
|
|
||||||
SharedValidator.RuleFor(s => s.QualityProfileId).ValidId();
|
SharedValidator.RuleFor(s => s.QualityProfileId).ValidId();
|
||||||
|
|
||||||
PutValidator.RuleFor(s => s.Path).IsValidPath();
|
PutValidator.RuleFor(s => s.Path)
|
||||||
|
.Cascade(CascadeMode.StopOnFirstFailure)
|
||||||
|
.IsValidPath()
|
||||||
|
.SetValidator(rootFolderValidator)
|
||||||
|
.SetValidator(pathExistsValidator)
|
||||||
|
.SetValidator(seriesPathValidator)
|
||||||
|
.SetValidator(droneFactoryValidator);
|
||||||
|
|
||||||
PostValidator.RuleFor(s => s.Path).IsValidPath().When(s => String.IsNullOrEmpty(s.RootFolderPath));
|
PostValidator.RuleFor(s => s.Path).IsValidPath().When(s => String.IsNullOrEmpty(s.RootFolderPath));
|
||||||
PostValidator.RuleFor(s => s.RootFolderPath).IsValidPath().When(s => String.IsNullOrEmpty(s.Path));
|
PostValidator.RuleFor(s => s.RootFolderPath).IsValidPath().When(s => String.IsNullOrEmpty(s.Path));
|
||||||
PostValidator.RuleFor(s => s.Title).NotEmpty();
|
PostValidator.RuleFor(s => s.Title).NotEmpty();
|
||||||
|
PostValidator.RuleFor(s => s.TvdbId).GreaterThan(0).SetValidator(seriesExistsValidator);
|
||||||
}
|
}
|
||||||
|
|
||||||
private SeriesResource GetSeries(int id)
|
private SeriesResource GetSeries(int id)
|
||||||
|
|
|
@ -13,6 +13,7 @@ namespace NzbDrone.Common
|
||||||
{
|
{
|
||||||
string DownloadString(string url);
|
string DownloadString(string url);
|
||||||
string DownloadString(string url, string username, string password);
|
string DownloadString(string url, string username, string password);
|
||||||
|
string DownloadString(string url, ICredentials credentials);
|
||||||
Dictionary<string, string> GetHeader(string url);
|
Dictionary<string, string> GetHeader(string url);
|
||||||
|
|
||||||
Stream DownloadStream(string url, NetworkCredential credential = null);
|
Stream DownloadStream(string url, NetworkCredential credential = null);
|
||||||
|
@ -44,7 +45,7 @@ namespace NzbDrone.Common
|
||||||
return DownloadString(url, new NetworkCredential(username, password));
|
return DownloadString(url, new NetworkCredential(username, password));
|
||||||
}
|
}
|
||||||
|
|
||||||
private string DownloadString(string url, ICredentials identity)
|
public string DownloadString(string url, ICredentials identity)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
|
@ -116,7 +116,7 @@ namespace NzbDrone.Common.Processes
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
logger.Info("Starting {0} {1}", path, args);
|
logger.Debug("Starting {0} {1}", path, args);
|
||||||
|
|
||||||
var process = new Process
|
var process = new Process
|
||||||
{
|
{
|
||||||
|
@ -163,7 +163,7 @@ namespace NzbDrone.Common.Processes
|
||||||
path = "mono";
|
path = "mono";
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.Info("Starting {0} {1}", path, args);
|
Logger.Debug("Starting {0} {1}", path, args);
|
||||||
|
|
||||||
var startInfo = new ProcessStartInfo(path, args);
|
var startInfo = new ProcessStartInfo(path, args);
|
||||||
var process = new Process
|
var process = new Process
|
||||||
|
|
|
@ -10,6 +10,7 @@ using NzbDrone.Core.Parser.Model;
|
||||||
using NzbDrone.Core.Test.Framework;
|
using NzbDrone.Core.Test.Framework;
|
||||||
using NzbDrone.Core.Tv;
|
using NzbDrone.Core.Tv;
|
||||||
using NzbDrone.Test.Common;
|
using NzbDrone.Test.Common;
|
||||||
|
using FizzWare.NBuilder;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Test.DecisionEngineTests
|
namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
{
|
{
|
||||||
|
@ -141,7 +142,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
results.Should().BeEmpty();
|
results.Should().BeEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test] public void should_not_attempt_to_map_episode_series_title_is_blank()
|
[Test]
|
||||||
|
public void should_not_attempt_to_map_episode_series_title_is_blank()
|
||||||
{
|
{
|
||||||
GivenSpecifications(_pass1, _pass2, _pass3);
|
GivenSpecifications(_pass1, _pass2, _pass3);
|
||||||
_reports[0].Title = "1937 - Snow White and the Seven Dwarves";
|
_reports[0].Title = "1937 - Snow White and the Seven Dwarves";
|
||||||
|
@ -204,5 +206,49 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
|
|
||||||
result.Should().HaveCount(1);
|
result.Should().HaveCount(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_only_include_reports_for_requested_episodes()
|
||||||
|
{
|
||||||
|
var series = Builder<Series>.CreateNew().Build();
|
||||||
|
|
||||||
|
var episodes = Builder<Episode>.CreateListOfSize(2)
|
||||||
|
.All()
|
||||||
|
.With(v => v.SeriesId, series.Id)
|
||||||
|
.With(v => v.Series, series)
|
||||||
|
.With(v => v.SeasonNumber, 1)
|
||||||
|
.With(v => v.SceneSeasonNumber, 2)
|
||||||
|
.BuildList();
|
||||||
|
|
||||||
|
var criteria = new SeasonSearchCriteria { Episodes = episodes.Take(1).ToList(), SeasonNumber = 1 };
|
||||||
|
|
||||||
|
var reports = episodes.Select(v =>
|
||||||
|
new ReleaseInfo()
|
||||||
|
{
|
||||||
|
Title = string.Format("{0}.S{1:00}E{2:00}.720p.WEB-DL-DRONE", series.Title, v.SceneSeasonNumber, v.SceneEpisodeNumber)
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
Mocker.GetMock<IParsingService>()
|
||||||
|
.Setup(v => v.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<SearchCriteriaBase>()))
|
||||||
|
.Returns<ParsedEpisodeInfo, int, SearchCriteriaBase>((p,id,c) =>
|
||||||
|
new RemoteEpisode
|
||||||
|
{
|
||||||
|
DownloadAllowed = true,
|
||||||
|
ParsedEpisodeInfo = p,
|
||||||
|
Series = series,
|
||||||
|
Episodes = episodes.Where(v => v.SceneEpisodeNumber == p.EpisodeNumbers.First()).ToList()
|
||||||
|
});
|
||||||
|
|
||||||
|
Mocker.SetConstant<IEnumerable<IDecisionEngineSpecification>>(new List<IDecisionEngineSpecification>
|
||||||
|
{
|
||||||
|
Mocker.Resolve<NzbDrone.Core.DecisionEngine.Specifications.Search.EpisodeRequestedSpecification>()
|
||||||
|
});
|
||||||
|
|
||||||
|
var decisions = Subject.GetSearchDecision(reports, criteria);
|
||||||
|
|
||||||
|
var approvedDecisions = decisions.Where(v => v.Approved).ToList();
|
||||||
|
|
||||||
|
approvedDecisions.Count.Should().Be(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,190 @@
|
||||||
|
using NzbDrone.Core.IndexerSearch;
|
||||||
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
using FizzWare.NBuilder;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Moq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Core.Tv;
|
||||||
|
using NzbDrone.Core.Indexers;
|
||||||
|
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.IndexerSearchTests
|
||||||
|
{
|
||||||
|
public class NzbSearchServiceFixture : CoreTest<NzbSearchService>
|
||||||
|
{
|
||||||
|
private Series _xemSeries;
|
||||||
|
private List<Episode> _xemEpisodes;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void SetUp()
|
||||||
|
{
|
||||||
|
var indexer = Mocker.GetMock<IIndexer>();
|
||||||
|
indexer.SetupGet(s => s.SupportsSearching).Returns(true);
|
||||||
|
|
||||||
|
Mocker.GetMock<IIndexerFactory>()
|
||||||
|
.Setup(s => s.GetAvailableProviders())
|
||||||
|
.Returns(new List<IIndexer> { indexer.Object });
|
||||||
|
|
||||||
|
Mocker.GetMock<NzbDrone.Core.DecisionEngine.IMakeDownloadDecision>()
|
||||||
|
.Setup(s => s.GetSearchDecision(It.IsAny<List<Parser.Model.ReleaseInfo>>(), It.IsAny<SearchCriteriaBase>()))
|
||||||
|
.Returns(new List<NzbDrone.Core.DecisionEngine.Specifications.DownloadDecision>());
|
||||||
|
|
||||||
|
_xemSeries = Builder<Series>.CreateNew()
|
||||||
|
.With(v => v.UseSceneNumbering = true)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
_xemEpisodes = new List<Episode>();
|
||||||
|
|
||||||
|
Mocker.GetMock<ISeriesService>()
|
||||||
|
.Setup(v => v.GetSeries(_xemSeries.Id))
|
||||||
|
.Returns(_xemSeries);
|
||||||
|
|
||||||
|
Mocker.GetMock<IEpisodeService>()
|
||||||
|
.Setup(v => v.GetEpisodesBySeason(_xemSeries.Id, It.IsAny<int>()))
|
||||||
|
.Returns<int, int>((i, j) => _xemEpisodes.Where(d => d.SeasonNumber == j).ToList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WithEpisode(int seasonNumber, int episodeNumber, int sceneSeasonNumber, int sceneEpisodeNumber)
|
||||||
|
{
|
||||||
|
var episode = Builder<Episode>.CreateNew()
|
||||||
|
.With(v => v.SeriesId == _xemSeries.Id)
|
||||||
|
.With(v => v.Series == _xemSeries)
|
||||||
|
.With(v => v.SeasonNumber, seasonNumber)
|
||||||
|
.With(v => v.EpisodeNumber, episodeNumber)
|
||||||
|
.With(v => v.SceneSeasonNumber, sceneSeasonNumber)
|
||||||
|
.With(v => v.SceneEpisodeNumber, sceneEpisodeNumber)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
_xemEpisodes.Add(episode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WithEpisodes()
|
||||||
|
{
|
||||||
|
// Season 1 maps to Scene Season 2 (one-to-one)
|
||||||
|
WithEpisode(1, 12, 2, 3);
|
||||||
|
WithEpisode(1, 13, 2, 4);
|
||||||
|
|
||||||
|
// Season 2 maps to Scene Season 3 & 4 (one-to-one)
|
||||||
|
WithEpisode(2, 1, 3, 11);
|
||||||
|
WithEpisode(2, 2, 3, 12);
|
||||||
|
WithEpisode(2, 3, 4, 11);
|
||||||
|
WithEpisode(2, 4, 4, 12);
|
||||||
|
|
||||||
|
// Season 3 maps to Scene Season 5 (partial)
|
||||||
|
// Season 4 maps to Scene Season 5 & 6 (partial)
|
||||||
|
WithEpisode(3, 1, 5, 11);
|
||||||
|
WithEpisode(3, 2, 5, 12);
|
||||||
|
WithEpisode(4, 1, 5, 13);
|
||||||
|
WithEpisode(4, 2, 5, 14);
|
||||||
|
WithEpisode(4, 3, 6, 11);
|
||||||
|
WithEpisode(5, 1, 6, 12);
|
||||||
|
|
||||||
|
// Season 7+ maps normally, so no mapping specified.
|
||||||
|
WithEpisode(7, 1, 0, 0);
|
||||||
|
WithEpisode(7, 2, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<SearchCriteriaBase> WatchForSearchCriteria()
|
||||||
|
{
|
||||||
|
List<SearchCriteriaBase> result = new List<SearchCriteriaBase>();
|
||||||
|
|
||||||
|
Mocker.GetMock<IFetchFeedFromIndexers>()
|
||||||
|
.Setup(v => v.Fetch(It.IsAny<IIndexer>(), It.IsAny<SingleEpisodeSearchCriteria>()))
|
||||||
|
.Callback<IIndexer, SingleEpisodeSearchCriteria>((i, s) => result.Add(s))
|
||||||
|
.Returns(new List<Parser.Model.ReleaseInfo>());
|
||||||
|
|
||||||
|
Mocker.GetMock<IFetchFeedFromIndexers>()
|
||||||
|
.Setup(v => v.Fetch(It.IsAny<IIndexer>(), It.IsAny<SeasonSearchCriteria>()))
|
||||||
|
.Callback<IIndexer, SeasonSearchCriteria>((i, s) => result.Add(s))
|
||||||
|
.Returns(new List<Parser.Model.ReleaseInfo>());
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void scene_episodesearch()
|
||||||
|
{
|
||||||
|
WithEpisodes();
|
||||||
|
|
||||||
|
var allCriteria = WatchForSearchCriteria();
|
||||||
|
|
||||||
|
Subject.EpisodeSearch(_xemEpisodes.First());
|
||||||
|
|
||||||
|
var criteria = allCriteria.OfType<SingleEpisodeSearchCriteria>().ToList();
|
||||||
|
|
||||||
|
criteria.Count.Should().Be(1);
|
||||||
|
criteria[0].SeasonNumber.Should().Be(2);
|
||||||
|
criteria[0].EpisodeNumber.Should().Be(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void scene_seasonsearch()
|
||||||
|
{
|
||||||
|
WithEpisodes();
|
||||||
|
|
||||||
|
var allCriteria = WatchForSearchCriteria();
|
||||||
|
|
||||||
|
Subject.SeasonSearch(_xemSeries.Id, 1);
|
||||||
|
|
||||||
|
var criteria = allCriteria.OfType<SeasonSearchCriteria>().ToList();
|
||||||
|
|
||||||
|
criteria.Count.Should().Be(1);
|
||||||
|
criteria[0].SeasonNumber.Should().Be(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void scene_seasonsearch_should_search_multiple_seasons()
|
||||||
|
{
|
||||||
|
WithEpisodes();
|
||||||
|
|
||||||
|
var allCriteria = WatchForSearchCriteria();
|
||||||
|
|
||||||
|
Subject.SeasonSearch(_xemSeries.Id, 2);
|
||||||
|
|
||||||
|
var criteria = allCriteria.OfType<SeasonSearchCriteria>().ToList();
|
||||||
|
|
||||||
|
criteria.Count.Should().Be(2);
|
||||||
|
criteria[0].SeasonNumber.Should().Be(3);
|
||||||
|
criteria[1].SeasonNumber.Should().Be(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void scene_seasonsearch_should_search_single_episode_if_possible()
|
||||||
|
{
|
||||||
|
WithEpisodes();
|
||||||
|
|
||||||
|
var allCriteria = WatchForSearchCriteria();
|
||||||
|
|
||||||
|
Subject.SeasonSearch(_xemSeries.Id, 4);
|
||||||
|
|
||||||
|
var criteria1 = allCriteria.OfType<SeasonSearchCriteria>().ToList();
|
||||||
|
var criteria2 = allCriteria.OfType<SingleEpisodeSearchCriteria>().ToList();
|
||||||
|
|
||||||
|
criteria1.Count.Should().Be(1);
|
||||||
|
criteria1[0].SeasonNumber.Should().Be(5);
|
||||||
|
|
||||||
|
criteria2.Count.Should().Be(1);
|
||||||
|
criteria2[0].SeasonNumber.Should().Be(6);
|
||||||
|
criteria2[0].EpisodeNumber.Should().Be(11);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void scene_seasonsearch_should_use_seasonnumber_if_no_scene_number_is_available()
|
||||||
|
{
|
||||||
|
WithEpisodes();
|
||||||
|
|
||||||
|
var allCriteria = WatchForSearchCriteria();
|
||||||
|
|
||||||
|
Subject.SeasonSearch(_xemSeries.Id, 7);
|
||||||
|
|
||||||
|
var criteria = allCriteria.OfType<SeasonSearchCriteria>().ToList();
|
||||||
|
|
||||||
|
criteria.Count.Should().Be(1);
|
||||||
|
criteria[0].SeasonNumber.Should().Be(7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ using NzbDrone.Core.MediaFiles.Commands;
|
||||||
using NzbDrone.Core.MediaFiles.EpisodeImport;
|
using NzbDrone.Core.MediaFiles.EpisodeImport;
|
||||||
using NzbDrone.Core.Parser;
|
using NzbDrone.Core.Parser;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
using NzbDrone.Core.Qualities;
|
||||||
using NzbDrone.Core.Test.Framework;
|
using NzbDrone.Core.Test.Framework;
|
||||||
using NzbDrone.Core.Tv;
|
using NzbDrone.Core.Tv;
|
||||||
using NzbDrone.Test.Common;
|
using NzbDrone.Test.Common;
|
||||||
|
@ -22,7 +23,7 @@ namespace NzbDrone.Core.Test.MediaFiles
|
||||||
public class DownloadedEpisodesImportServiceFixture : CoreTest<DownloadedEpisodesImportService>
|
public class DownloadedEpisodesImportServiceFixture : CoreTest<DownloadedEpisodesImportService>
|
||||||
{
|
{
|
||||||
private string[] _subFolders = new[] { "c:\\root\\foldername".AsOsAgnostic() };
|
private string[] _subFolders = new[] { "c:\\root\\foldername".AsOsAgnostic() };
|
||||||
private string[] _videoFiles = new[] { "c:\\root\\foldername\\video.ext".AsOsAgnostic() };
|
private string[] _videoFiles = new[] { "c:\\root\\foldername\\30.rock.s01e01.ext".AsOsAgnostic() };
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
public void Setup()
|
public void Setup()
|
||||||
|
@ -113,6 +114,8 @@ namespace NzbDrone.Core.Test.MediaFiles
|
||||||
|
|
||||||
Mocker.GetMock<IParsingService>()
|
Mocker.GetMock<IParsingService>()
|
||||||
.Verify(v => v.GetSeries(It.IsAny<String>()), Times.Never());
|
.Verify(v => v.GetSeries(It.IsAny<String>()), Times.Never());
|
||||||
|
|
||||||
|
ExceptionVerification.ExpectedWarns(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
@ -129,7 +132,7 @@ namespace NzbDrone.Core.Test.MediaFiles
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void should_delete_folder_if_files_were_imported()
|
public void should_delete_folder_if_files_were_imported_and_video_files_remain()
|
||||||
{
|
{
|
||||||
GivenValidSeries();
|
GivenValidSeries();
|
||||||
|
|
||||||
|
@ -148,6 +151,40 @@ namespace NzbDrone.Core.Test.MediaFiles
|
||||||
|
|
||||||
Subject.Execute(new DownloadedEpisodesScanCommand());
|
Subject.Execute(new DownloadedEpisodesScanCommand());
|
||||||
|
|
||||||
|
Mocker.GetMock<IDiskProvider>()
|
||||||
|
.Verify(v => v.DeleteFolder(It.IsAny<String>(), true), Times.Never());
|
||||||
|
|
||||||
|
ExceptionVerification.ExpectedWarns(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_delete_folder_if_files_were_imported_and_only_sample_files_remain()
|
||||||
|
{
|
||||||
|
GivenValidSeries();
|
||||||
|
|
||||||
|
var localEpisode = new LocalEpisode();
|
||||||
|
|
||||||
|
var imported = new List<ImportDecision>();
|
||||||
|
imported.Add(new ImportDecision(localEpisode));
|
||||||
|
|
||||||
|
Mocker.GetMock<IMakeImportDecision>()
|
||||||
|
.Setup(s => s.GetImportDecisions(It.IsAny<List<String>>(), It.IsAny<Series>(), true, null))
|
||||||
|
.Returns(imported);
|
||||||
|
|
||||||
|
Mocker.GetMock<IImportApprovedEpisodes>()
|
||||||
|
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true))
|
||||||
|
.Returns(imported);
|
||||||
|
|
||||||
|
Mocker.GetMock<ISampleService>()
|
||||||
|
.Setup(s => s.IsSample(It.IsAny<Series>(),
|
||||||
|
It.IsAny<QualityModel>(),
|
||||||
|
It.IsAny<String>(),
|
||||||
|
It.IsAny<Int64>(),
|
||||||
|
It.IsAny<Int32>()))
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
Subject.Execute(new DownloadedEpisodesScanCommand());
|
||||||
|
|
||||||
Mocker.GetMock<IDiskProvider>()
|
Mocker.GetMock<IDiskProvider>()
|
||||||
.Verify(v => v.DeleteFolder(It.IsAny<String>(), true), Times.Once());
|
.Verify(v => v.DeleteFolder(It.IsAny<String>(), true), Times.Once());
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,144 @@
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using FizzWare.NBuilder;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Moq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Core.MediaFiles.EpisodeImport;
|
||||||
|
using NzbDrone.Core.MediaFiles.MediaInfo;
|
||||||
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
using NzbDrone.Core.Qualities;
|
||||||
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
using NzbDrone.Core.Tv;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class SampleServiceFixture : CoreTest<SampleService>
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
Quality = new QualityModel(Quality.HDTV720p)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenFileSize(long size)
|
||||||
|
{
|
||||||
|
_localEpisode.Size = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenRuntime(int seconds)
|
||||||
|
{
|
||||||
|
Mocker.GetMock<IVideoFileInfoReader>()
|
||||||
|
.Setup(s => s.GetRunTime(It.IsAny<String>()))
|
||||||
|
.Returns(new TimeSpan(0, 0, seconds));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_false_if_season_zero()
|
||||||
|
{
|
||||||
|
_localEpisode.Episodes[0].SeasonNumber = 0;
|
||||||
|
ShouldBeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_false_for_flv()
|
||||||
|
{
|
||||||
|
_localEpisode.Path = @"C:\Test\some.show.s01e01.flv";
|
||||||
|
|
||||||
|
ShouldBeFalse();
|
||||||
|
|
||||||
|
Mocker.GetMock<IVideoFileInfoReader>().Verify(c => c.GetRunTime(It.IsAny<string>()), Times.Never());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_use_runtime()
|
||||||
|
{
|
||||||
|
GivenRuntime(120);
|
||||||
|
GivenFileSize(1000.Megabytes());
|
||||||
|
|
||||||
|
Subject.IsSample(_localEpisode.Series,
|
||||||
|
_localEpisode.Quality,
|
||||||
|
_localEpisode.Path,
|
||||||
|
_localEpisode.Size,
|
||||||
|
_localEpisode.SeasonNumber);
|
||||||
|
|
||||||
|
Mocker.GetMock<IVideoFileInfoReader>().Verify(v => v.GetRunTime(It.IsAny<String>()), Times.Once());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_true_if_runtime_is_less_than_minimum()
|
||||||
|
{
|
||||||
|
GivenRuntime(60);
|
||||||
|
|
||||||
|
ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_false_if_runtime_greater_than_than_minimum()
|
||||||
|
{
|
||||||
|
GivenRuntime(120);
|
||||||
|
|
||||||
|
ShouldBeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_fall_back_to_file_size_if_mediainfo_dll_not_found_acceptable_size()
|
||||||
|
{
|
||||||
|
Mocker.GetMock<IVideoFileInfoReader>()
|
||||||
|
.Setup(s => s.GetRunTime(It.IsAny<String>()))
|
||||||
|
.Throws<DllNotFoundException>();
|
||||||
|
|
||||||
|
GivenFileSize(1000.Megabytes());
|
||||||
|
ShouldBeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_fall_back_to_file_size_if_mediainfo_dll_not_found_undersize()
|
||||||
|
{
|
||||||
|
Mocker.GetMock<IVideoFileInfoReader>()
|
||||||
|
.Setup(s => s.GetRunTime(It.IsAny<String>()))
|
||||||
|
.Throws<DllNotFoundException>();
|
||||||
|
|
||||||
|
GivenFileSize(1.Megabytes());
|
||||||
|
ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShouldBeTrue()
|
||||||
|
{
|
||||||
|
Subject.IsSample(_localEpisode.Series,
|
||||||
|
_localEpisode.Quality,
|
||||||
|
_localEpisode.Path,
|
||||||
|
_localEpisode.Size,
|
||||||
|
_localEpisode.SeasonNumber).Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShouldBeFalse()
|
||||||
|
{
|
||||||
|
Subject.IsSample(_localEpisode.Series,
|
||||||
|
_localEpisode.Quality,
|
||||||
|
_localEpisode.Path,
|
||||||
|
_localEpisode.Size,
|
||||||
|
_localEpisode.SeasonNumber).Should().BeFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ using Moq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NzbDrone.Common;
|
using NzbDrone.Common;
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.MediaFiles.EpisodeImport.Specifications;
|
using NzbDrone.Core.MediaFiles.EpisodeImport.Specifications;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
using NzbDrone.Core.Test.Framework;
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
@ -143,5 +144,15 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
|
||||||
|
|
||||||
Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue();
|
Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_true_when_skip_check_is_enabled()
|
||||||
|
{
|
||||||
|
Mocker.GetMock<IConfigService>()
|
||||||
|
.Setup(s => s.SkipFreeSpaceCheckWhenImporting)
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,96 +41,11 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private void GivenFileSize(long size)
|
|
||||||
{
|
|
||||||
_localEpisode.Size = size;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void GivenRuntime(int seconds)
|
|
||||||
{
|
|
||||||
Mocker.GetMock<IVideoFileInfoReader>()
|
|
||||||
.Setup(s => s.GetRunTime(It.IsAny<String>()))
|
|
||||||
.Returns(new TimeSpan(0, 0, seconds));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void should_return_true_if_series_is_daily()
|
|
||||||
{
|
|
||||||
_series.SeriesType = SeriesTypes.Daily;
|
|
||||||
Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void should_return_true_if_season_zero()
|
|
||||||
{
|
|
||||||
_localEpisode.Episodes[0].SeasonNumber = 0;
|
|
||||||
Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void should_return_true_for_existing_file()
|
public void should_return_true_for_existing_file()
|
||||||
{
|
{
|
||||||
_localEpisode.ExistingFile = true;
|
_localEpisode.ExistingFile = true;
|
||||||
Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue();
|
Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void should_return_true_for_flv()
|
|
||||||
{
|
|
||||||
_localEpisode.Path = @"C:\Test\some.show.s01e01.flv";
|
|
||||||
|
|
||||||
Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue();
|
|
||||||
|
|
||||||
Mocker.GetMock<IVideoFileInfoReader>().Verify(c => c.GetRunTime(It.IsAny<string>()), Times.Never());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void should_use_runtime()
|
|
||||||
{
|
|
||||||
GivenRuntime(120);
|
|
||||||
GivenFileSize(1000.Megabytes());
|
|
||||||
|
|
||||||
Subject.IsSatisfiedBy(_localEpisode);
|
|
||||||
|
|
||||||
Mocker.GetMock<IVideoFileInfoReader>().Verify(v => v.GetRunTime(It.IsAny<String>()), Times.Once());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void should_return_false_if_runtime_is_less_than_minimum()
|
|
||||||
{
|
|
||||||
GivenRuntime(60);
|
|
||||||
|
|
||||||
Subject.IsSatisfiedBy(_localEpisode).Should().BeFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void should_return_true_if_runtime_greater_than_than_minimum()
|
|
||||||
{
|
|
||||||
GivenRuntime(120);
|
|
||||||
|
|
||||||
Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void should_fall_back_to_file_size_if_mediainfo_dll_not_found_acceptable_size()
|
|
||||||
{
|
|
||||||
Mocker.GetMock<IVideoFileInfoReader>()
|
|
||||||
.Setup(s => s.GetRunTime(It.IsAny<String>()))
|
|
||||||
.Throws<DllNotFoundException>();
|
|
||||||
|
|
||||||
GivenFileSize(1000.Megabytes());
|
|
||||||
Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void should_fall_back_to_file_size_if_mediainfo_dll_not_found_undersize()
|
|
||||||
{
|
|
||||||
Mocker.GetMock<IVideoFileInfoReader>()
|
|
||||||
.Setup(s => s.GetRunTime(It.IsAny<String>()))
|
|
||||||
.Throws<DllNotFoundException>();
|
|
||||||
|
|
||||||
GivenFileSize(1.Megabytes());
|
|
||||||
Subject.IsSatisfiedBy(_localEpisode).Should().BeFalse();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,7 +96,9 @@ namespace NzbDrone.Core.Test.MetadataSourceTests
|
||||||
private void ValidateEpisode(Episode episode)
|
private void ValidateEpisode(Episode episode)
|
||||||
{
|
{
|
||||||
episode.Should().NotBeNull();
|
episode.Should().NotBeNull();
|
||||||
episode.EpisodeNumber.Should().NotBe(0);
|
|
||||||
|
//TODO: Is there a better way to validate that episode number or season number is greater than zero?
|
||||||
|
(episode.EpisodeNumber + episode.SeasonNumber).Should().NotBe(0);
|
||||||
|
|
||||||
episode.Should().NotBeNull();
|
episode.Should().NotBeNull();
|
||||||
|
|
||||||
|
|
|
@ -138,6 +138,7 @@
|
||||||
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedMetadataFilesFixture.cs" />
|
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedMetadataFilesFixture.cs" />
|
||||||
<Compile Include="Housekeeping\Housekeepers\CleanupDuplicateMetadataFilesFixture.cs" />
|
<Compile Include="Housekeeping\Housekeepers\CleanupDuplicateMetadataFilesFixture.cs" />
|
||||||
<Compile Include="Housekeeping\Housekeepers\FixFutureRunScheduledTasksFixture.cs" />
|
<Compile Include="Housekeeping\Housekeepers\FixFutureRunScheduledTasksFixture.cs" />
|
||||||
|
<Compile Include="IndexerSearchTests\NzbSearchServiceFixture.cs" />
|
||||||
<Compile Include="IndexerSearchTests\SearchDefinitionFixture.cs" />
|
<Compile Include="IndexerSearchTests\SearchDefinitionFixture.cs" />
|
||||||
<Compile Include="IndexerTests\BasicRssParserFixture.cs" />
|
<Compile Include="IndexerTests\BasicRssParserFixture.cs" />
|
||||||
<Compile Include="IndexerTests\IndexerServiceFixture.cs" />
|
<Compile Include="IndexerTests\IndexerServiceFixture.cs" />
|
||||||
|
@ -155,6 +156,7 @@
|
||||||
<Compile Include="MediaFiles\EpisodeImport\Specifications\FreeSpaceSpecificationFixture.cs" />
|
<Compile Include="MediaFiles\EpisodeImport\Specifications\FreeSpaceSpecificationFixture.cs" />
|
||||||
<Compile Include="MediaFiles\EpisodeImport\ImportDecisionMakerFixture.cs" />
|
<Compile Include="MediaFiles\EpisodeImport\ImportDecisionMakerFixture.cs" />
|
||||||
<Compile Include="MediaFiles\EpisodeImport\Specifications\NotInUseSpecificationFixture.cs" />
|
<Compile Include="MediaFiles\EpisodeImport\Specifications\NotInUseSpecificationFixture.cs" />
|
||||||
|
<Compile Include="MediaFiles\EpisodeImport\SampleServiceFixture.cs" />
|
||||||
<Compile Include="MediaFiles\EpisodeImport\Specifications\NotSampleSpecificationFixture.cs" />
|
<Compile Include="MediaFiles\EpisodeImport\Specifications\NotSampleSpecificationFixture.cs" />
|
||||||
<Compile Include="MediaFiles\EpisodeImport\Specifications\NotUnpackingSpecificationFixture.cs" />
|
<Compile Include="MediaFiles\EpisodeImport\Specifications\NotUnpackingSpecificationFixture.cs" />
|
||||||
<Compile Include="MediaFiles\EpisodeImport\Specifications\UpgradeSpecificationFixture.cs" />
|
<Compile Include="MediaFiles\EpisodeImport\Specifications\UpgradeSpecificationFixture.cs" />
|
||||||
|
@ -212,6 +214,7 @@
|
||||||
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedEpisodesFixture.cs" />
|
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedEpisodesFixture.cs" />
|
||||||
<Compile Include="ThingiProviderTests\NullConfigFixture.cs" />
|
<Compile Include="ThingiProviderTests\NullConfigFixture.cs" />
|
||||||
<Compile Include="ThingiProvider\ProviderBaseFixture.cs" />
|
<Compile Include="ThingiProvider\ProviderBaseFixture.cs" />
|
||||||
|
<Compile Include="TvTests\EpisodeRepositoryTests\ByAirDateFixture.cs" />
|
||||||
<Compile Include="TvTests\EpisodeRepositoryTests\EpisodesWithFilesFixture.cs" />
|
<Compile Include="TvTests\EpisodeRepositoryTests\EpisodesWithFilesFixture.cs" />
|
||||||
<Compile Include="TvTests\EpisodeRepositoryTests\EpisodesWhereCutoffUnmetFixture.cs" />
|
<Compile Include="TvTests\EpisodeRepositoryTests\EpisodesWhereCutoffUnmetFixture.cs" />
|
||||||
<Compile Include="TvTests\RefreshEpisodeServiceFixture.cs" />
|
<Compile Include="TvTests\RefreshEpisodeServiceFixture.cs" />
|
||||||
|
|
|
@ -39,6 +39,7 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||||
[TestCase("Ray Donovan - S01E01.720p.HDtv.x264-Evolve (NLsub)", Language.Dutch)]
|
[TestCase("Ray Donovan - S01E01.720p.HDtv.x264-Evolve (NLsub)", Language.Dutch)]
|
||||||
[TestCase("Shield,.The.1x13.Tueurs.De.Flics.FR.DVDRip.XviD", Language.French)]
|
[TestCase("Shield,.The.1x13.Tueurs.De.Flics.FR.DVDRip.XviD", Language.French)]
|
||||||
[TestCase("True.Detective.S01E01.1080p.WEB-DL.Rus.Eng.TVKlondike", Language.Russian)]
|
[TestCase("True.Detective.S01E01.1080p.WEB-DL.Rus.Eng.TVKlondike", Language.Russian)]
|
||||||
|
[TestCase("The.Trip.To.Italy.S02E01.720p.HDTV.x264-TLA", Language.English)]
|
||||||
public void should_parse_language(string postTitle, Language language)
|
public void should_parse_language(string postTitle, Language language)
|
||||||
{
|
{
|
||||||
var result = Parser.Parser.ParseTitle(postTitle);
|
var result = Parser.Parser.ParseTitle(postTitle);
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
using System;
|
||||||
|
using FizzWare.NBuilder;
|
||||||
|
using FluentAssertions;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
using NzbDrone.Core.Tv;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class ByAirDateFixture : DbTest<EpisodeRepository, Episode>
|
||||||
|
{
|
||||||
|
private const int SERIES_ID = 1;
|
||||||
|
private const string AIR_DATE = "2014-04-02";
|
||||||
|
|
||||||
|
private void GivenEpisode(int seasonNumber)
|
||||||
|
{
|
||||||
|
var episode = Builder<Episode>.CreateNew()
|
||||||
|
.With(e => e.SeriesId = 1)
|
||||||
|
.With(e => e.SeasonNumber = seasonNumber)
|
||||||
|
.With(e => e.AirDate = AIR_DATE)
|
||||||
|
.BuildNew();
|
||||||
|
|
||||||
|
Db.Insert(episode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_throw_when_multiple_regular_episodes_are_found()
|
||||||
|
{
|
||||||
|
GivenEpisode(1);
|
||||||
|
GivenEpisode(2);
|
||||||
|
|
||||||
|
Assert.Throws<InvalidOperationException>(() => Subject.Get(SERIES_ID, AIR_DATE));
|
||||||
|
Assert.Throws<InvalidOperationException>(() => Subject.Find(SERIES_ID, AIR_DATE));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_throw_when_get_finds_no_episode()
|
||||||
|
{
|
||||||
|
Assert.Throws<InvalidOperationException>(() => Subject.Get(SERIES_ID, AIR_DATE));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_get_episode_when_single_episode_exists_for_air_date()
|
||||||
|
{
|
||||||
|
GivenEpisode(1);
|
||||||
|
|
||||||
|
Subject.Get(SERIES_ID, AIR_DATE).Should().NotBeNull();
|
||||||
|
Subject.Find(SERIES_ID, AIR_DATE).Should().NotBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_get_episode_when_regular_episode_and_special_share_the_same_air_date()
|
||||||
|
{
|
||||||
|
GivenEpisode(1);
|
||||||
|
GivenEpisode(0);
|
||||||
|
|
||||||
|
Subject.Get(SERIES_ID, AIR_DATE).Should().NotBeNull();
|
||||||
|
Subject.Find(SERIES_ID, AIR_DATE).Should().NotBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_get_special_when_its_the_only_episode_for_the_date_provided()
|
||||||
|
{
|
||||||
|
GivenEpisode(0);
|
||||||
|
|
||||||
|
Subject.Get(SERIES_ID, AIR_DATE).Should().NotBeNull();
|
||||||
|
Subject.Find(SERIES_ID, AIR_DATE).Should().NotBeNull();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -164,6 +164,13 @@ namespace NzbDrone.Core.Configuration
|
||||||
set { SetValue("DownloadedEpisodesScanInterval", value); }
|
set { SetValue("DownloadedEpisodesScanInterval", value); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Boolean SkipFreeSpaceCheckWhenImporting
|
||||||
|
{
|
||||||
|
get { return GetValueBoolean("SkipFreeSpaceCheckWhenImporting", false); }
|
||||||
|
|
||||||
|
set { SetValue("SkipFreeSpaceCheckWhenImporting", value); }
|
||||||
|
}
|
||||||
|
|
||||||
public Boolean SetPermissionsLinux
|
public Boolean SetPermissionsLinux
|
||||||
{
|
{
|
||||||
get { return GetValueBoolean("SetPermissionsLinux", false); }
|
get { return GetValueBoolean("SetPermissionsLinux", false); }
|
||||||
|
|
|
@ -26,6 +26,7 @@ namespace NzbDrone.Core.Configuration
|
||||||
Boolean AutoDownloadPropers { get; set; }
|
Boolean AutoDownloadPropers { get; set; }
|
||||||
Boolean CreateEmptySeriesFolders { get; set; }
|
Boolean CreateEmptySeriesFolders { get; set; }
|
||||||
FileDateType FileDate { get; set; }
|
FileDateType FileDate { get; set; }
|
||||||
|
Boolean SkipFreeSpaceCheckWhenImporting { get; set; }
|
||||||
|
|
||||||
//Permissions (Media Management)
|
//Permissions (Media Management)
|
||||||
Boolean SetPermissionsLinux { get; set; }
|
Boolean SetPermissionsLinux { get; set; }
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
using System.Linq;
|
||||||
|
using NLog;
|
||||||
|
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||||
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.DecisionEngine.Specifications.Search
|
||||||
|
{
|
||||||
|
public class EpisodeRequestedSpecification : IDecisionEngineSpecification
|
||||||
|
{
|
||||||
|
private readonly Logger _logger;
|
||||||
|
|
||||||
|
public EpisodeRequestedSpecification(Logger logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string RejectionReason
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return "Episode wasn't requested";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria)
|
||||||
|
{
|
||||||
|
if (searchCriteria == null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
var criteriaEpisodes = searchCriteria.Episodes.Select(v => v.Id).ToList();
|
||||||
|
var remoteEpisodes = remoteEpisode.Episodes.Select(v => v.Id).ToList();
|
||||||
|
if (!criteriaEpisodes.Intersect(remoteEpisodes).Any())
|
||||||
|
{
|
||||||
|
_logger.Debug("Release rejected since the episode wasn't requested: {0}", remoteEpisode.ParsedEpisodeInfo);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,7 +4,6 @@ using System.Linq;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||||
using NzbDrone.Core.Qualities;
|
using NzbDrone.Core.Qualities;
|
||||||
using NzbDrone.Core.Tv;
|
|
||||||
|
|
||||||
namespace NzbDrone.Core.Download
|
namespace NzbDrone.Core.Download
|
||||||
{
|
{
|
||||||
|
|
|
@ -3,7 +3,7 @@ using NzbDrone.Core.Messaging.Commands;
|
||||||
|
|
||||||
namespace NzbDrone.Core.IndexerSearch
|
namespace NzbDrone.Core.IndexerSearch
|
||||||
{
|
{
|
||||||
public class MissingEpisodeSearchCommand : Command
|
public class EpisodeSearchCommand : Command
|
||||||
{
|
{
|
||||||
public List<int> EpisodeIds { get; set; }
|
public List<int> EpisodeIds { get; set; }
|
||||||
|
|
||||||
|
|
|
@ -50,7 +50,7 @@ namespace NzbDrone.Core.IndexerSearch
|
||||||
PageSize = 100000,
|
PageSize = 100000,
|
||||||
SortDirection = SortDirection.Ascending,
|
SortDirection = SortDirection.Ascending,
|
||||||
SortKey = "Id",
|
SortKey = "Id",
|
||||||
FilterExpression = v => v.Monitored && v.Series.Monitored
|
FilterExpression = v => v.Monitored == true && v.Series.Monitored == true
|
||||||
}).Records.ToList();
|
}).Records.ToList();
|
||||||
|
|
||||||
_logger.ProgressInfo("Performing missing search for {0} episodes", episodes.Count);
|
_logger.ProgressInfo("Performing missing search for {0} episodes", episodes.Count);
|
||||||
|
|
|
@ -3,7 +3,7 @@ using NzbDrone.Core.Messaging.Commands;
|
||||||
|
|
||||||
namespace NzbDrone.Core.IndexerSearch
|
namespace NzbDrone.Core.IndexerSearch
|
||||||
{
|
{
|
||||||
public class EpisodeSearchCommand : Command
|
public class MissingEpisodeSearchCommand : Command
|
||||||
{
|
{
|
||||||
public List<int> EpisodeIds { get; set; }
|
public List<int> EpisodeIds { get; set; }
|
||||||
|
|
||||||
|
|
|
@ -138,10 +138,53 @@ namespace NzbDrone.Core.IndexerSearch
|
||||||
return SearchSpecial(series, episodes);
|
return SearchSpecial(series, episodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
var searchSpec = Get<SeasonSearchCriteria>(series, episodes);
|
List<DownloadDecision> downloadDecisions = new List<DownloadDecision>();
|
||||||
searchSpec.SeasonNumber = seasonNumber;
|
|
||||||
|
|
||||||
return Dispatch(indexer => _feedFetcher.Fetch(indexer, searchSpec), searchSpec);
|
if (series.UseSceneNumbering)
|
||||||
|
{
|
||||||
|
var sceneSeasonGroups = episodes.GroupBy(v =>
|
||||||
|
{
|
||||||
|
if (v.SceneSeasonNumber == 0 && v.SceneEpisodeNumber == 0)
|
||||||
|
return v.SeasonNumber;
|
||||||
|
else
|
||||||
|
return v.SceneSeasonNumber;
|
||||||
|
}).Distinct();
|
||||||
|
|
||||||
|
foreach (var sceneSeasonEpisodes in sceneSeasonGroups)
|
||||||
|
{
|
||||||
|
if (sceneSeasonEpisodes.Count() == 1)
|
||||||
|
{
|
||||||
|
var episode = sceneSeasonEpisodes.First();
|
||||||
|
var searchSpec = Get<SingleEpisodeSearchCriteria>(series, sceneSeasonEpisodes.ToList());
|
||||||
|
searchSpec.SeasonNumber = sceneSeasonEpisodes.Key;
|
||||||
|
if (episode.SceneSeasonNumber == 0 && episode.SceneEpisodeNumber == 0)
|
||||||
|
searchSpec.EpisodeNumber = episode.EpisodeNumber;
|
||||||
|
else
|
||||||
|
searchSpec.EpisodeNumber = episode.SceneEpisodeNumber;
|
||||||
|
|
||||||
|
var decisions = Dispatch(indexer => _feedFetcher.Fetch(indexer, searchSpec), searchSpec);
|
||||||
|
downloadDecisions.AddRange(decisions);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var searchSpec = Get<SeasonSearchCriteria>(series, sceneSeasonEpisodes.ToList());
|
||||||
|
searchSpec.SeasonNumber = sceneSeasonEpisodes.Key;
|
||||||
|
|
||||||
|
var decisions = Dispatch(indexer => _feedFetcher.Fetch(indexer, searchSpec), searchSpec);
|
||||||
|
downloadDecisions.AddRange(decisions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var searchSpec = Get<SeasonSearchCriteria>(series, episodes);
|
||||||
|
searchSpec.SeasonNumber = seasonNumber;
|
||||||
|
|
||||||
|
var decisions = Dispatch(indexer => _feedFetcher.Fetch(indexer, searchSpec), searchSpec);
|
||||||
|
downloadDecisions.AddRange(decisions);
|
||||||
|
}
|
||||||
|
|
||||||
|
return downloadDecisions;
|
||||||
}
|
}
|
||||||
|
|
||||||
private TSpec Get<TSpec>(Series series, List<Episode> episodes) where TSpec : SearchCriteriaBase, new()
|
private TSpec Get<TSpec>(Series series, List<Episode> episodes) where TSpec : SearchCriteriaBase, new()
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System;
|
||||||
using NzbDrone.Core.Messaging.Commands;
|
using NzbDrone.Core.Messaging.Commands;
|
||||||
|
|
||||||
namespace NzbDrone.Core.MediaFiles.Commands
|
namespace NzbDrone.Core.MediaFiles.Commands
|
||||||
|
@ -12,6 +13,7 @@ namespace NzbDrone.Core.MediaFiles.Commands
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool SendUpdates { get; set; }
|
public Boolean SendUpdates { get; set; }
|
||||||
|
public String Path { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,10 +1,12 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common;
|
using NzbDrone.Common;
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
|
using NzbDrone.Common.EnsureThat;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.MediaFiles.Commands;
|
using NzbDrone.Core.MediaFiles.Commands;
|
||||||
using NzbDrone.Core.MediaFiles.EpisodeImport;
|
using NzbDrone.Core.MediaFiles.EpisodeImport;
|
||||||
|
@ -24,6 +26,7 @@ namespace NzbDrone.Core.MediaFiles
|
||||||
private readonly IConfigService _configService;
|
private readonly IConfigService _configService;
|
||||||
private readonly IMakeImportDecision _importDecisionMaker;
|
private readonly IMakeImportDecision _importDecisionMaker;
|
||||||
private readonly IImportApprovedEpisodes _importApprovedEpisodes;
|
private readonly IImportApprovedEpisodes _importApprovedEpisodes;
|
||||||
|
private readonly ISampleService _sampleService;
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
|
|
||||||
public DownloadedEpisodesImportService(IDiskProvider diskProvider,
|
public DownloadedEpisodesImportService(IDiskProvider diskProvider,
|
||||||
|
@ -33,6 +36,7 @@ namespace NzbDrone.Core.MediaFiles
|
||||||
IConfigService configService,
|
IConfigService configService,
|
||||||
IMakeImportDecision importDecisionMaker,
|
IMakeImportDecision importDecisionMaker,
|
||||||
IImportApprovedEpisodes importApprovedEpisodes,
|
IImportApprovedEpisodes importApprovedEpisodes,
|
||||||
|
ISampleService sampleService,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
{
|
{
|
||||||
_diskProvider = diskProvider;
|
_diskProvider = diskProvider;
|
||||||
|
@ -42,6 +46,7 @@ namespace NzbDrone.Core.MediaFiles
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
_importDecisionMaker = importDecisionMaker;
|
_importDecisionMaker = importDecisionMaker;
|
||||||
_importApprovedEpisodes = importApprovedEpisodes;
|
_importApprovedEpisodes = importApprovedEpisodes;
|
||||||
|
_sampleService = sampleService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,24 +69,7 @@ namespace NzbDrone.Core.MediaFiles
|
||||||
|
|
||||||
foreach (var subFolder in _diskProvider.GetDirectories(downloadedEpisodesFolder))
|
foreach (var subFolder in _diskProvider.GetDirectories(downloadedEpisodesFolder))
|
||||||
{
|
{
|
||||||
try
|
ProcessFolder(subFolder);
|
||||||
{
|
|
||||||
if (_seriesService.SeriesPathExists(subFolder))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var importedFiles = ProcessSubFolder(new DirectoryInfo(subFolder));
|
|
||||||
|
|
||||||
if (importedFiles.Any())
|
|
||||||
{
|
|
||||||
_diskProvider.DeleteFolder(subFolder, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
_logger.ErrorException("An error has occurred while importing folder: " + subFolder, e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var videoFile in _diskScanService.GetVideoFiles(downloadedEpisodesFolder, false))
|
foreach (var videoFile in _diskScanService.GetVideoFiles(downloadedEpisodesFolder, false))
|
||||||
|
@ -97,9 +85,9 @@ namespace NzbDrone.Core.MediaFiles
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<ImportDecision> ProcessSubFolder(DirectoryInfo subfolderInfo)
|
private List<ImportDecision> ProcessFolder(DirectoryInfo directoryInfo)
|
||||||
{
|
{
|
||||||
var cleanedUpName = GetCleanedUpFolderName(subfolderInfo.Name);
|
var cleanedUpName = GetCleanedUpFolderName(directoryInfo.Name);
|
||||||
var series = _parsingService.GetSeries(cleanedUpName);
|
var series = _parsingService.GetSeries(cleanedUpName);
|
||||||
var quality = QualityParser.ParseQuality(cleanedUpName);
|
var quality = QualityParser.ParseQuality(cleanedUpName);
|
||||||
_logger.Debug("{0} folder quality: {1}", cleanedUpName, quality);
|
_logger.Debug("{0} folder quality: {1}", cleanedUpName, quality);
|
||||||
|
@ -110,7 +98,7 @@ namespace NzbDrone.Core.MediaFiles
|
||||||
return new List<ImportDecision>();
|
return new List<ImportDecision>();
|
||||||
}
|
}
|
||||||
|
|
||||||
var videoFiles = _diskScanService.GetVideoFiles(subfolderInfo.FullName);
|
var videoFiles = _diskScanService.GetVideoFiles(directoryInfo.FullName);
|
||||||
|
|
||||||
return ProcessFiles(series, quality, videoFiles);
|
return ProcessFiles(series, quality, videoFiles);
|
||||||
}
|
}
|
||||||
|
@ -140,6 +128,33 @@ namespace NzbDrone.Core.MediaFiles
|
||||||
return _importApprovedEpisodes.Import(decisions, true);
|
return _importApprovedEpisodes.Import(decisions, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ProcessFolder(string path)
|
||||||
|
{
|
||||||
|
Ensure.That(path, () => path).IsValidPath();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_seriesService.SeriesPathExists(path))
|
||||||
|
{
|
||||||
|
_logger.Warn("Unable to process folder that contains sorted TV Shows");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var directoryFolderInfo = new DirectoryInfo(path);
|
||||||
|
var importedFiles = ProcessFolder(directoryFolderInfo);
|
||||||
|
|
||||||
|
if (importedFiles.Any() && ShouldDeleteFolder(directoryFolderInfo))
|
||||||
|
{
|
||||||
|
_logger.Debug("Deleting folder after importing valid files");
|
||||||
|
_diskProvider.DeleteFolder(path, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.ErrorException("An error has occurred while importing folder: " + path, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private string GetCleanedUpFolderName(string folder)
|
private string GetCleanedUpFolderName(string folder)
|
||||||
{
|
{
|
||||||
folder = folder.Replace("_UNPACK_", "")
|
folder = folder.Replace("_UNPACK_", "")
|
||||||
|
@ -148,9 +163,47 @@ namespace NzbDrone.Core.MediaFiles
|
||||||
return folder;
|
return folder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool ShouldDeleteFolder(DirectoryInfo directoryInfo)
|
||||||
|
{
|
||||||
|
var videoFiles = _diskScanService.GetVideoFiles(directoryInfo.FullName);
|
||||||
|
var cleanedUpName = GetCleanedUpFolderName(directoryInfo.Name);
|
||||||
|
var series = _parsingService.GetSeries(cleanedUpName);
|
||||||
|
|
||||||
|
foreach (var videoFile in videoFiles)
|
||||||
|
{
|
||||||
|
var episodeParseResult = Parser.Parser.ParseTitle(Path.GetFileName(videoFile));
|
||||||
|
|
||||||
|
if (episodeParseResult == null)
|
||||||
|
{
|
||||||
|
_logger.Warn("Unable to parse file on import: [{0}]", videoFile);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var size = _diskProvider.GetFileSize(videoFile);
|
||||||
|
var quality = QualityParser.ParseQuality(videoFile);
|
||||||
|
|
||||||
|
if (!_sampleService.IsSample(series, quality, videoFile, size,
|
||||||
|
episodeParseResult.SeasonNumber))
|
||||||
|
{
|
||||||
|
_logger.Warn("Non-sample file detected: [{0}]", videoFile);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public void Execute(DownloadedEpisodesScanCommand message)
|
public void Execute(DownloadedEpisodesScanCommand message)
|
||||||
{
|
{
|
||||||
ProcessDownloadedEpisodesFolder();
|
if (message.Path.IsNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
ProcessDownloadedEpisodesFolder();
|
||||||
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ProcessFolder(message.Path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using NLog;
|
||||||
|
using NzbDrone.Core.MediaFiles.MediaInfo;
|
||||||
|
using NzbDrone.Core.Qualities;
|
||||||
|
using NzbDrone.Core.Tv;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.MediaFiles.EpisodeImport
|
||||||
|
{
|
||||||
|
public interface ISampleService
|
||||||
|
{
|
||||||
|
bool IsSample(Series series, QualityModel quality, string path, long size, int seasonNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SampleService : ISampleService
|
||||||
|
{
|
||||||
|
private readonly IVideoFileInfoReader _videoFileInfoReader;
|
||||||
|
private readonly Logger _logger;
|
||||||
|
|
||||||
|
private static List<Quality> _largeSampleSizeQualities = new List<Quality> { Quality.HDTV1080p, Quality.WEBDL1080p, Quality.Bluray1080p };
|
||||||
|
|
||||||
|
public SampleService(IVideoFileInfoReader videoFileInfoReader, Logger logger)
|
||||||
|
{
|
||||||
|
_videoFileInfoReader = videoFileInfoReader;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long SampleSizeLimit
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return 70.Megabytes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsSample(Series series, QualityModel quality, string path, long size, int seasonNumber)
|
||||||
|
{
|
||||||
|
if (seasonNumber == 0)
|
||||||
|
{
|
||||||
|
_logger.Debug("Special, skipping sample check");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var extension = Path.GetExtension(path);
|
||||||
|
|
||||||
|
if (extension != null && extension.Equals(".flv", StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
_logger.Debug("Skipping sample check for .flv file");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var runTime = _videoFileInfoReader.GetRunTime(path);
|
||||||
|
|
||||||
|
if (runTime.TotalMinutes.Equals(0))
|
||||||
|
{
|
||||||
|
_logger.Error("[{0}] has a runtime of 0, is it a valid video file?", path);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runTime.TotalSeconds < 90)
|
||||||
|
{
|
||||||
|
_logger.Debug("[{0}] appears to be a sample. Size: {1} Runtime: {2}", path, size, runTime);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
catch (DllNotFoundException)
|
||||||
|
{
|
||||||
|
_logger.Debug("Falling back to file size detection");
|
||||||
|
|
||||||
|
return CheckSize(size, quality);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.Debug("Runtime is over 90 seconds");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CheckSize(long size, QualityModel quality)
|
||||||
|
{
|
||||||
|
if (_largeSampleSizeQualities.Contains(quality.Quality))
|
||||||
|
{
|
||||||
|
if (size < SampleSizeLimit * 2)
|
||||||
|
{
|
||||||
|
_logger.Debug("1080p file is less than sample limit");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (size < SampleSizeLimit)
|
||||||
|
{
|
||||||
|
_logger.Debug("File is less than sample limit");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common;
|
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
|
||||||
namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
|
namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
|
||||||
|
@ -10,11 +10,13 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
|
||||||
public class FreeSpaceSpecification : IImportDecisionEngineSpecification
|
public class FreeSpaceSpecification : IImportDecisionEngineSpecification
|
||||||
{
|
{
|
||||||
private readonly IDiskProvider _diskProvider;
|
private readonly IDiskProvider _diskProvider;
|
||||||
|
private readonly IConfigService _configService;
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
|
|
||||||
public FreeSpaceSpecification(IDiskProvider diskProvider, Logger logger)
|
public FreeSpaceSpecification(IDiskProvider diskProvider, IConfigService configService, Logger logger)
|
||||||
{
|
{
|
||||||
_diskProvider = diskProvider;
|
_diskProvider = diskProvider;
|
||||||
|
_configService = configService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,6 +24,12 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
|
||||||
|
|
||||||
public bool IsSatisfiedBy(LocalEpisode localEpisode)
|
public bool IsSatisfiedBy(LocalEpisode localEpisode)
|
||||||
{
|
{
|
||||||
|
if (_configService.SkipFreeSpaceCheckWhenImporting)
|
||||||
|
{
|
||||||
|
_logger.Debug("Skipping free space check when importing");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (localEpisode.ExistingFile)
|
if (localEpisode.ExistingFile)
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
|
||||||
using NzbDrone.Core.MediaFiles.MediaInfo;
|
using NzbDrone.Core.MediaFiles.MediaInfo;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
using NzbDrone.Core.Qualities;
|
using NzbDrone.Core.Qualities;
|
||||||
|
@ -12,25 +11,16 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
|
||||||
{
|
{
|
||||||
public class NotSampleSpecification : IImportDecisionEngineSpecification
|
public class NotSampleSpecification : IImportDecisionEngineSpecification
|
||||||
{
|
{
|
||||||
private readonly IVideoFileInfoReader _videoFileInfoReader;
|
private readonly ISampleService _sampleService;
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
private static List<Quality> _largeSampleSizeQualities = new List<Quality> { Quality.HDTV1080p, Quality.WEBDL1080p, Quality.Bluray1080p };
|
|
||||||
|
|
||||||
public NotSampleSpecification(IVideoFileInfoReader videoFileInfoReader,
|
public NotSampleSpecification(ISampleService sampleService,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
{
|
{
|
||||||
_videoFileInfoReader = videoFileInfoReader;
|
_sampleService = sampleService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static long SampleSizeLimit
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return 70.Megabytes();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string RejectionReason { get { return "Sample"; } }
|
public string RejectionReason { get { return "Sample"; } }
|
||||||
|
|
||||||
public bool IsSatisfiedBy(LocalEpisode localEpisode)
|
public bool IsSatisfiedBy(LocalEpisode localEpisode)
|
||||||
|
@ -41,72 +31,11 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (localEpisode.Series.SeriesType == SeriesTypes.Daily)
|
return !_sampleService.IsSample(localEpisode.Series,
|
||||||
{
|
localEpisode.Quality,
|
||||||
_logger.Debug("Daily Series, skipping sample check");
|
localEpisode.Path,
|
||||||
return true;
|
localEpisode.Size,
|
||||||
}
|
localEpisode.SeasonNumber);
|
||||||
|
|
||||||
if (localEpisode.SeasonNumber == 0)
|
|
||||||
{
|
|
||||||
_logger.Debug("Special, skipping sample check");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var extension = Path.GetExtension(localEpisode.Path);
|
|
||||||
|
|
||||||
if (extension != null && extension.Equals(".flv", StringComparison.InvariantCultureIgnoreCase))
|
|
||||||
{
|
|
||||||
_logger.Debug("Skipping sample check for .flv file");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var runTime = _videoFileInfoReader.GetRunTime(localEpisode.Path);
|
|
||||||
|
|
||||||
if (runTime.TotalMinutes.Equals(0))
|
|
||||||
{
|
|
||||||
_logger.Error("[{0}] has a runtime of 0, is it a valid video file?", localEpisode);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (runTime.TotalSeconds < 90)
|
|
||||||
{
|
|
||||||
_logger.Debug("[{0}] appears to be a sample. Size: {1} Runtime: {2}", localEpisode.Path, localEpisode.Size, runTime);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
catch (DllNotFoundException)
|
|
||||||
{
|
|
||||||
_logger.Debug("Falling back to file size detection");
|
|
||||||
|
|
||||||
return CheckSize(localEpisode);
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.Debug("Runtime is over 90 seconds");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool CheckSize(LocalEpisode localEpisode)
|
|
||||||
{
|
|
||||||
if (_largeSampleSizeQualities.Contains(localEpisode.Quality.Quality))
|
|
||||||
{
|
|
||||||
if (localEpisode.Size < SampleSizeLimit * 2)
|
|
||||||
{
|
|
||||||
_logger.Debug("1080p file is less than sample limit");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (localEpisode.Size < SampleSizeLimit)
|
|
||||||
{
|
|
||||||
_logger.Debug("File is less than sample limit");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -193,16 +193,22 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc
|
||||||
{
|
{
|
||||||
metadata.Type = MetadataType.SeasonImage;
|
metadata.Type = MetadataType.SeasonImage;
|
||||||
|
|
||||||
var seasonNumber = seasonMatch.Groups["season"].Value;
|
var seasonNumberMatch = seasonMatch.Groups["season"].Value;
|
||||||
|
int seasonNumber;
|
||||||
|
|
||||||
if (seasonNumber.Contains("specials"))
|
if (seasonNumberMatch.Contains("specials"))
|
||||||
{
|
{
|
||||||
metadata.SeasonNumber = 0;
|
metadata.SeasonNumber = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
else if (Int32.TryParse(seasonNumberMatch, out seasonNumber))
|
||||||
|
{
|
||||||
|
metadata.SeasonNumber = seasonNumber;
|
||||||
|
}
|
||||||
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
metadata.SeasonNumber = Convert.ToInt32(seasonNumber);
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return metadata;
|
return metadata;
|
||||||
|
@ -313,14 +319,16 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc
|
||||||
}
|
}
|
||||||
|
|
||||||
_diskProvider.CopyFile(source, destination, false);
|
_diskProvider.CopyFile(source, destination, false);
|
||||||
|
var relativePath = DiskProviderBase.GetRelativePath(series.Path, destination);
|
||||||
|
|
||||||
var metadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.SeriesImage) ??
|
var metadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.SeriesImage &&
|
||||||
|
c.RelativePath == relativePath) ??
|
||||||
new MetadataFile
|
new MetadataFile
|
||||||
{
|
{
|
||||||
SeriesId = series.Id,
|
SeriesId = series.Id,
|
||||||
Consumer = GetType().Name,
|
Consumer = GetType().Name,
|
||||||
Type = MetadataType.SeriesImage,
|
Type = MetadataType.SeriesImage,
|
||||||
RelativePath = DiskProviderBase.GetRelativePath(series.Path, destination)
|
RelativePath = relativePath
|
||||||
};
|
};
|
||||||
|
|
||||||
yield return metadata;
|
yield return metadata;
|
||||||
|
@ -341,18 +349,20 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc
|
||||||
}
|
}
|
||||||
|
|
||||||
var path = Path.Combine(series.Path, filename);
|
var path = Path.Combine(series.Path, filename);
|
||||||
|
var relativePath = DiskProviderBase.GetRelativePath(series.Path, path);
|
||||||
|
|
||||||
DownloadImage(series, image.Url, path);
|
DownloadImage(series, image.Url, path);
|
||||||
|
|
||||||
var metadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.SeasonImage &&
|
var metadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.SeasonImage &&
|
||||||
c.SeasonNumber == season.SeasonNumber) ??
|
c.SeasonNumber == season.SeasonNumber &&
|
||||||
|
c.RelativePath == relativePath) ??
|
||||||
new MetadataFile
|
new MetadataFile
|
||||||
{
|
{
|
||||||
SeriesId = series.Id,
|
SeriesId = series.Id,
|
||||||
SeasonNumber = season.SeasonNumber,
|
SeasonNumber = season.SeasonNumber,
|
||||||
Consumer = GetType().Name,
|
Consumer = GetType().Name,
|
||||||
Type = MetadataType.SeasonImage,
|
Type = MetadataType.SeasonImage,
|
||||||
RelativePath = DiskProviderBase.GetRelativePath(series.Path, path)
|
RelativePath = relativePath
|
||||||
};
|
};
|
||||||
|
|
||||||
yield return metadata;
|
yield return metadata;
|
||||||
|
@ -458,7 +468,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc
|
||||||
var filename = GetEpisodeImageFilename(episodeFile.Path);
|
var filename = GetEpisodeImageFilename(episodeFile.Path);
|
||||||
var relativePath = DiskProviderBase.GetRelativePath(series.Path, filename);
|
var relativePath = DiskProviderBase.GetRelativePath(series.Path, filename);
|
||||||
|
|
||||||
var existingMetadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.EpisodeImage &&
|
var existingMetadata = existingMetadataFiles.FirstOrDefault(c => c.Type == MetadataType.EpisodeImage &&
|
||||||
c.EpisodeFileId == episodeFile.Id);
|
c.EpisodeFileId == episodeFile.Id);
|
||||||
|
|
||||||
if (existingMetadata != null)
|
if (existingMetadata != null)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using Growl.Connector;
|
using Growl.Connector;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.Instrumentation;
|
using NzbDrone.Common.Instrumentation;
|
||||||
|
@ -63,6 +64,8 @@ namespace NzbDrone.Core.Notifications.Growl
|
||||||
const string title = "Test Notification";
|
const string title = "Test Notification";
|
||||||
const string body = "This is a test message from NzbDrone";
|
const string body = "This is a test message from NzbDrone";
|
||||||
|
|
||||||
|
Thread.Sleep(5000);
|
||||||
|
|
||||||
SendNotification(title, body, "TEST", message.Host, message.Port, message.Password);
|
SendNotification(title, body, "TEST", message.Host, message.Port, message.Password);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,13 @@ namespace NzbDrone.Core.Notifications.Plex
|
||||||
[FieldDefinition(1, Label = "Port")]
|
[FieldDefinition(1, Label = "Port")]
|
||||||
public Int32 Port { get; set; }
|
public Int32 Port { get; set; }
|
||||||
|
|
||||||
[FieldDefinition(2, Label = "Update Library", Type = FieldType.Checkbox)]
|
[FieldDefinition(2, Label = "Username")]
|
||||||
|
public String Username { get; set; }
|
||||||
|
|
||||||
|
[FieldDefinition(3, Label = "Password")]
|
||||||
|
public String Password { get; set; }
|
||||||
|
|
||||||
|
[FieldDefinition(4, Label = "Update Library", Type = FieldType.Checkbox)]
|
||||||
public Boolean UpdateLibrary { get; set; }
|
public Boolean UpdateLibrary { get; set; }
|
||||||
|
|
||||||
public bool IsValid
|
public bool IsValid
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
using System.Xml.Linq;
|
using System.Xml.Linq;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common;
|
using NzbDrone.Common;
|
||||||
|
@ -58,7 +59,7 @@ namespace NzbDrone.Core.Notifications.Plex
|
||||||
{
|
{
|
||||||
_logger.Debug("Getting sections from Plex host: {0}", settings.Host);
|
_logger.Debug("Getting sections from Plex host: {0}", settings.Host);
|
||||||
var url = String.Format("http://{0}:{1}/library/sections", settings.Host, settings.Port);
|
var url = String.Format("http://{0}:{1}/library/sections", settings.Host, settings.Port);
|
||||||
var xmlStream = _httpProvider.DownloadStream(url, null);
|
var xmlStream = _httpProvider.DownloadStream(url, GetCredentials(settings));
|
||||||
var xDoc = XDocument.Load(xmlStream);
|
var xDoc = XDocument.Load(xmlStream);
|
||||||
var mediaContainer = xDoc.Descendants("MediaContainer").FirstOrDefault();
|
var mediaContainer = xDoc.Descendants("MediaContainer").FirstOrDefault();
|
||||||
var directories = mediaContainer.Descendants("Directory").Where(x => x.Attribute("type").Value == "show");
|
var directories = mediaContainer.Descendants("Directory").Where(x => x.Attribute("type").Value == "show");
|
||||||
|
@ -70,7 +71,7 @@ namespace NzbDrone.Core.Notifications.Plex
|
||||||
{
|
{
|
||||||
_logger.Debug("Updating Plex host: {0}, Section: {1}", settings.Host, key);
|
_logger.Debug("Updating Plex host: {0}, Section: {1}", settings.Host, key);
|
||||||
var url = String.Format("http://{0}:{1}/library/sections/{2}/refresh", settings.Host, settings.Port, key);
|
var url = String.Format("http://{0}:{1}/library/sections/{2}/refresh", settings.Host, settings.Port, key);
|
||||||
_httpProvider.DownloadString(url);
|
_httpProvider.DownloadString(url, GetCredentials(settings));
|
||||||
}
|
}
|
||||||
|
|
||||||
public string SendCommand(string host, int port, string command, string username, string password)
|
public string SendCommand(string host, int port, string command, string username, string password)
|
||||||
|
@ -85,6 +86,13 @@ namespace NzbDrone.Core.Notifications.Plex
|
||||||
return _httpProvider.DownloadString(url);
|
return _httpProvider.DownloadString(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private NetworkCredential GetCredentials(PlexServerSettings settings)
|
||||||
|
{
|
||||||
|
if (settings.Username.IsNullOrWhiteSpace()) return null;
|
||||||
|
|
||||||
|
return new NetworkCredential(settings.Username, settings.Password);
|
||||||
|
}
|
||||||
|
|
||||||
public void Execute(TestPlexClientCommand message)
|
public void Execute(TestPlexClientCommand message)
|
||||||
{
|
{
|
||||||
_logger.Debug("Sending Test Notifcation to Plex Client: {0}", message.Host);
|
_logger.Debug("Sending Test Notifcation to Plex Client: {0}", message.Host);
|
||||||
|
@ -100,7 +108,13 @@ namespace NzbDrone.Core.Notifications.Plex
|
||||||
|
|
||||||
public void Execute(TestPlexServerCommand message)
|
public void Execute(TestPlexServerCommand message)
|
||||||
{
|
{
|
||||||
if (!GetSectionKeys(new PlexServerSettings {Host = message.Host, Port = message.Port}).Any())
|
if (!GetSectionKeys(new PlexServerSettings
|
||||||
|
{
|
||||||
|
Host = message.Host,
|
||||||
|
Port = message.Port,
|
||||||
|
Username = message.Username,
|
||||||
|
Password = message.Password
|
||||||
|
}).Any())
|
||||||
{
|
{
|
||||||
throw new Exception("Unable to connect to Plex Server");
|
throw new Exception("Unable to connect to Plex Server");
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,5 +14,7 @@ namespace NzbDrone.Core.Notifications.Plex
|
||||||
|
|
||||||
public string Host { get; set; }
|
public string Host { get; set; }
|
||||||
public int Port { get; set; }
|
public int Port { get; set; }
|
||||||
|
public string Username { get; set; }
|
||||||
|
public string Password { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,6 @@ namespace NzbDrone.Core.Notifications.PushBullet
|
||||||
public PushBulletSettingsValidator()
|
public PushBulletSettingsValidator()
|
||||||
{
|
{
|
||||||
RuleFor(c => c.ApiKey).NotEmpty();
|
RuleFor(c => c.ApiKey).NotEmpty();
|
||||||
RuleFor(c => c.DeviceId).NotEmpty();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,7 +21,7 @@ namespace NzbDrone.Core.Notifications.PushBullet
|
||||||
[FieldDefinition(0, Label = "API Key", HelpLink = "https://www.pushbullet.com/")]
|
[FieldDefinition(0, Label = "API Key", HelpLink = "https://www.pushbullet.com/")]
|
||||||
public String ApiKey { get; set; }
|
public String ApiKey { get; set; }
|
||||||
|
|
||||||
[FieldDefinition(1, Label = "Device ID", HelpText = "device_iden in the device's URL on pubshbullet.com")]
|
[FieldDefinition(1, Label = "Device ID", HelpText = "device_iden in the device's URL on pubshbullet.com (leave blank to send to all devices)")]
|
||||||
public String DeviceId { get; set; }
|
public String DeviceId { get; set; }
|
||||||
|
|
||||||
public bool IsValid
|
public bool IsValid
|
||||||
|
|
|
@ -20,14 +20,14 @@ namespace NzbDrone.Core.Notifications.Pushover
|
||||||
{
|
{
|
||||||
const string title = "Episode Grabbed";
|
const string title = "Episode Grabbed";
|
||||||
|
|
||||||
_pushoverProxy.SendNotification(title, message, Settings.ApiKey, Settings.UserKey, (PushoverPriority)Settings.Priority);
|
_pushoverProxy.SendNotification(title, message, Settings.ApiKey, Settings.UserKey, (PushoverPriority)Settings.Priority, Settings.Sound);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnDownload(DownloadMessage message)
|
public override void OnDownload(DownloadMessage message)
|
||||||
{
|
{
|
||||||
const string title = "Episode Downloaded";
|
const string title = "Episode Downloaded";
|
||||||
|
|
||||||
_pushoverProxy.SendNotification(title, message.Message, Settings.ApiKey, Settings.UserKey, (PushoverPriority)Settings.Priority);
|
_pushoverProxy.SendNotification(title, message.Message, Settings.ApiKey, Settings.UserKey, (PushoverPriority)Settings.Priority, Settings.Sound);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void AfterRename(Series series)
|
public override void AfterRename(Series series)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using NzbDrone.Core.Messaging.Commands;
|
using NzbDrone.Common;
|
||||||
|
using NzbDrone.Core.Messaging.Commands;
|
||||||
using RestSharp;
|
using RestSharp;
|
||||||
using NzbDrone.Core.Rest;
|
using NzbDrone.Core.Rest;
|
||||||
|
|
||||||
|
@ -6,14 +7,14 @@ namespace NzbDrone.Core.Notifications.Pushover
|
||||||
{
|
{
|
||||||
public interface IPushoverProxy
|
public interface IPushoverProxy
|
||||||
{
|
{
|
||||||
void SendNotification(string title, string message, string apiKey, string userKey, PushoverPriority priority);
|
void SendNotification(string title, string message, string apiKey, string userKey, PushoverPriority priority, string sound);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PushoverProxy : IPushoverProxy, IExecute<TestPushoverCommand>
|
public class PushoverProxy : IPushoverProxy, IExecute<TestPushoverCommand>
|
||||||
{
|
{
|
||||||
private const string URL = "https://api.pushover.net/1/messages.json";
|
private const string URL = "https://api.pushover.net/1/messages.json";
|
||||||
|
|
||||||
public void SendNotification(string title, string message, string apiKey, string userKey, PushoverPriority priority)
|
public void SendNotification(string title, string message, string apiKey, string userKey, PushoverPriority priority, string sound)
|
||||||
{
|
{
|
||||||
var client = new RestClient(URL);
|
var client = new RestClient(URL);
|
||||||
var request = new RestRequest(Method.POST);
|
var request = new RestRequest(Method.POST);
|
||||||
|
@ -23,6 +24,9 @@ namespace NzbDrone.Core.Notifications.Pushover
|
||||||
request.AddParameter("message", message);
|
request.AddParameter("message", message);
|
||||||
request.AddParameter("priority", (int)priority);
|
request.AddParameter("priority", (int)priority);
|
||||||
|
|
||||||
|
if (!sound.IsNullOrWhiteSpace()) request.AddParameter("sound", sound);
|
||||||
|
|
||||||
|
|
||||||
client.ExecuteAndValidate(request);
|
client.ExecuteAndValidate(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,7 +35,7 @@ namespace NzbDrone.Core.Notifications.Pushover
|
||||||
const string title = "Test Notification";
|
const string title = "Test Notification";
|
||||||
const string body = "This is a test message from NzbDrone";
|
const string body = "This is a test message from NzbDrone";
|
||||||
|
|
||||||
SendNotification(title, body, message.ApiKey, message.UserKey, (PushoverPriority)message.Priority);
|
SendNotification(title, body, message.ApiKey, message.UserKey, (PushoverPriority)message.Priority, message.Sound);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,9 @@ namespace NzbDrone.Core.Notifications.Pushover
|
||||||
[FieldDefinition(2, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(PushoverPriority) )]
|
[FieldDefinition(2, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(PushoverPriority) )]
|
||||||
public Int32 Priority { get; set; }
|
public Int32 Priority { get; set; }
|
||||||
|
|
||||||
|
[FieldDefinition(3, Label = "Sound", Type = FieldType.Textbox, HelpText = "Notification sound, leave blank to use the default", HelpLink = "https://pushover.net/api#sounds")]
|
||||||
|
public String Sound { get; set; }
|
||||||
|
|
||||||
public bool IsValid
|
public bool IsValid
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|
|
@ -16,5 +16,6 @@ namespace NzbDrone.Core.Notifications.Pushover
|
||||||
public string ApiKey { get; set; }
|
public string ApiKey { get; set; }
|
||||||
public string UserKey { get; set; }
|
public string UserKey { get; set; }
|
||||||
public int Priority { get; set; }
|
public int Priority { get; set; }
|
||||||
|
public string Sound { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -217,6 +217,7 @@
|
||||||
<Compile Include="DecisionEngine\Specifications\NotRestrictedReleaseSpecification.cs" />
|
<Compile Include="DecisionEngine\Specifications\NotRestrictedReleaseSpecification.cs" />
|
||||||
<Compile Include="DecisionEngine\Specifications\NotSampleSpecification.cs" />
|
<Compile Include="DecisionEngine\Specifications\NotSampleSpecification.cs" />
|
||||||
<Compile Include="DecisionEngine\Specifications\RssSync\ProperSpecification.cs" />
|
<Compile Include="DecisionEngine\Specifications\RssSync\ProperSpecification.cs" />
|
||||||
|
<Compile Include="DecisionEngine\Specifications\Search\EpisodeRequestedSpecification.cs" />
|
||||||
<Compile Include="DecisionEngine\Specifications\Search\SeriesSpecification.cs" />
|
<Compile Include="DecisionEngine\Specifications\Search\SeriesSpecification.cs" />
|
||||||
<Compile Include="DecisionEngine\Specifications\Search\SeasonMatchSpecification.cs" />
|
<Compile Include="DecisionEngine\Specifications\Search\SeasonMatchSpecification.cs" />
|
||||||
<Compile Include="DecisionEngine\Specifications\Search\DailyEpisodeMatchSpecification.cs" />
|
<Compile Include="DecisionEngine\Specifications\Search\DailyEpisodeMatchSpecification.cs" />
|
||||||
|
@ -326,6 +327,7 @@
|
||||||
<Compile Include="MediaCover\MediaCoversUpdatedEvent.cs" />
|
<Compile Include="MediaCover\MediaCoversUpdatedEvent.cs" />
|
||||||
<Compile Include="MediaFiles\Commands\RenameFilesCommand.cs" />
|
<Compile Include="MediaFiles\Commands\RenameFilesCommand.cs" />
|
||||||
<Compile Include="MediaFiles\EpisodeFileMoveResult.cs" />
|
<Compile Include="MediaFiles\EpisodeFileMoveResult.cs" />
|
||||||
|
<Compile Include="MediaFiles\EpisodeImport\SampleService.cs" />
|
||||||
<Compile Include="MediaFiles\EpisodeImport\Specifications\FullSeasonSpecification.cs" />
|
<Compile Include="MediaFiles\EpisodeImport\Specifications\FullSeasonSpecification.cs" />
|
||||||
<Compile Include="MediaFiles\Events\SeriesScannedEvent.cs" />
|
<Compile Include="MediaFiles\Events\SeriesScannedEvent.cs" />
|
||||||
<Compile Include="MediaFiles\FileDateType.cs" />
|
<Compile Include="MediaFiles\FileDateType.cs" />
|
||||||
|
@ -680,9 +682,11 @@
|
||||||
<Compile Include="Update\UpdatePackageProvider.cs" />
|
<Compile Include="Update\UpdatePackageProvider.cs" />
|
||||||
<Compile Include="Update\UpdatePackage.cs" />
|
<Compile Include="Update\UpdatePackage.cs" />
|
||||||
<Compile Include="Update\UpdateCheckService.cs" />
|
<Compile Include="Update\UpdateCheckService.cs" />
|
||||||
|
<Compile Include="Validation\Paths\SeriesExistsValidator.cs" />
|
||||||
|
<Compile Include="Validation\Paths\RootFolderValidator.cs" />
|
||||||
<Compile Include="Validation\Paths\DroneFactoryValidator.cs" />
|
<Compile Include="Validation\Paths\DroneFactoryValidator.cs" />
|
||||||
<Compile Include="Validation\Paths\PathValidator.cs" />
|
<Compile Include="Validation\Paths\PathValidator.cs" />
|
||||||
<Compile Include="Validation\Paths\RootFolderValidator.cs" />
|
<Compile Include="Validation\Paths\SeriesPathValidator.cs" />
|
||||||
<Compile Include="Validation\Paths\PathExistsValidator.cs" />
|
<Compile Include="Validation\Paths\PathExistsValidator.cs" />
|
||||||
<Compile Include="Validation\FolderValidator.cs" />
|
<Compile Include="Validation\FolderValidator.cs" />
|
||||||
<Compile Include="Validation\RuleBuilderExtensions.cs" />
|
<Compile Include="Validation\RuleBuilderExtensions.cs" />
|
||||||
|
|
|
@ -107,7 +107,7 @@ namespace NzbDrone.Core.Parser
|
||||||
|
|
||||||
private static readonly Regex MultiPartCleanupRegex = new Regex(@"\(\d+\)$", RegexOptions.Compiled);
|
private static readonly Regex MultiPartCleanupRegex = new Regex(@"\(\d+\)$", RegexOptions.Compiled);
|
||||||
|
|
||||||
private static readonly Regex LanguageRegex = new Regex(@"(?:\W|_)(?<italian>ita|italian)|(?<german>german\b)|(?<flemish>flemish)|(?<greek>greek)|(?<french>(?:\W|_)FR)(?:\W|_)|(?<russian>\brus\b)",
|
private static readonly Regex LanguageRegex = new Regex(@"(?:\W|_)(?<italian>\bita\b|italian)|(?<german>german\b)|(?<flemish>flemish)|(?<greek>greek)|(?<french>(?:\W|_)FR)(?:\W|_)|(?<russian>\brus\b)",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||||
|
|
||||||
private static readonly Regex YearInTitleRegex = new Regex(@"^(?<title>.+?)(?:\W|_)?(?<year>\d{4})",
|
private static readonly Regex YearInTitleRegex = new Regex(@"^(?<title>.+?)(?:\W|_)?(?<year>\d{4})",
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Marr.Data.QGen;
|
using Marr.Data.QGen;
|
||||||
|
using NLog;
|
||||||
using NzbDrone.Core.Datastore;
|
using NzbDrone.Core.Datastore;
|
||||||
using NzbDrone.Core.Datastore.Extentions;
|
using NzbDrone.Core.Datastore.Extentions;
|
||||||
using NzbDrone.Core.Messaging.Events;
|
using NzbDrone.Core.Messaging.Events;
|
||||||
|
@ -14,8 +16,8 @@ namespace NzbDrone.Core.Tv
|
||||||
{
|
{
|
||||||
Episode Find(int seriesId, int season, int episodeNumber);
|
Episode Find(int seriesId, int season, int episodeNumber);
|
||||||
Episode Find(int seriesId, int absoluteEpisodeNumber);
|
Episode Find(int seriesId, int absoluteEpisodeNumber);
|
||||||
Episode Get(int seriesId, String date);
|
Episode Get(int seriesId, string date);
|
||||||
Episode Find(int seriesId, String date);
|
Episode Find(int seriesId, string date);
|
||||||
List<Episode> GetEpisodes(int seriesId);
|
List<Episode> GetEpisodes(int seriesId);
|
||||||
List<Episode> GetEpisodes(int seriesId, int seasonNumber);
|
List<Episode> GetEpisodes(int seriesId, int seasonNumber);
|
||||||
List<Episode> GetEpisodeByFileId(int fileId);
|
List<Episode> GetEpisodeByFileId(int fileId);
|
||||||
|
@ -32,11 +34,13 @@ namespace NzbDrone.Core.Tv
|
||||||
public class EpisodeRepository : BasicRepository<Episode>, IEpisodeRepository
|
public class EpisodeRepository : BasicRepository<Episode>, IEpisodeRepository
|
||||||
{
|
{
|
||||||
private readonly IDatabase _database;
|
private readonly IDatabase _database;
|
||||||
|
private readonly Logger _logger;
|
||||||
|
|
||||||
public EpisodeRepository(IDatabase database, IEventAggregator eventAggregator)
|
public EpisodeRepository(IDatabase database, IEventAggregator eventAggregator, Logger logger)
|
||||||
: base(database, eventAggregator)
|
: base(database, eventAggregator)
|
||||||
{
|
{
|
||||||
_database = database;
|
_database = database;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Episode Find(int seriesId, int season, int episodeNumber)
|
public Episode Find(int seriesId, int season, int episodeNumber)
|
||||||
|
@ -54,18 +58,21 @@ namespace NzbDrone.Core.Tv
|
||||||
.SingleOrDefault();
|
.SingleOrDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Episode Get(int seriesId, String date)
|
public Episode Get(int seriesId, string date)
|
||||||
{
|
{
|
||||||
return Query.Where(s => s.SeriesId == seriesId)
|
var episode = FindOneByAirDate(seriesId, date);
|
||||||
.AndWhere(s => s.AirDate == date)
|
|
||||||
.Single();
|
if (episode == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Expected at one episode");
|
||||||
|
}
|
||||||
|
|
||||||
|
return episode;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Episode Find(int seriesId, String date)
|
public Episode Find(int seriesId, string date)
|
||||||
{
|
{
|
||||||
return Query.Where(s => s.SeriesId == seriesId)
|
return FindOneByAirDate(seriesId, date);
|
||||||
.AndWhere(s => s.AirDate == date)
|
|
||||||
.SingleOrDefault();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Episode> GetEpisodes(int seriesId)
|
public List<Episode> GetEpisodes(int seriesId)
|
||||||
|
@ -207,5 +214,28 @@ namespace NzbDrone.Core.Tv
|
||||||
|
|
||||||
return String.Format("({0})", String.Join(" OR ", clauses));
|
return String.Format("({0})", String.Join(" OR ", clauses));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Episode FindOneByAirDate(int seriesId, string date)
|
||||||
|
{
|
||||||
|
var episodes = Query.Where(s => s.SeriesId == seriesId)
|
||||||
|
.AndWhere(s => s.AirDate == date)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (!episodes.Any()) return null;
|
||||||
|
|
||||||
|
if (episodes.Count == 1) return episodes.First();
|
||||||
|
|
||||||
|
_logger.Debug("Multiple episodes with the same air date were found, will exclude specials");
|
||||||
|
|
||||||
|
var regularEpisodes = episodes.Where(e => e.SeasonNumber > 0).ToList();
|
||||||
|
|
||||||
|
if (regularEpisodes.Count == 1)
|
||||||
|
{
|
||||||
|
_logger.Debug("Left with one episode after excluding specials");
|
||||||
|
return regularEpisodes.First();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException("Multiple episodes with the same air date found");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NLog;
|
using NLog;
|
||||||
|
using NzbDrone.Common;
|
||||||
using NzbDrone.Common.EnsureThat;
|
using NzbDrone.Common.EnsureThat;
|
||||||
using NzbDrone.Core.DataAugmentation.Scene;
|
using NzbDrone.Core.DataAugmentation.Scene;
|
||||||
using NzbDrone.Core.Messaging.Events;
|
using NzbDrone.Core.Messaging.Events;
|
||||||
|
@ -197,14 +198,22 @@ namespace NzbDrone.Core.Tv
|
||||||
{
|
{
|
||||||
foreach (var s in series)
|
foreach (var s in series)
|
||||||
{
|
{
|
||||||
if (!String.IsNullOrWhiteSpace(s.RootFolderPath))
|
if (!s.RootFolderPath.IsNullOrWhiteSpace())
|
||||||
{
|
{
|
||||||
var folderName = new DirectoryInfo(s.Path).Name;
|
var folderName = new DirectoryInfo(s.Path).Name;
|
||||||
s.Path = Path.Combine(s.RootFolderPath, folderName);
|
s.Path = Path.Combine(s.RootFolderPath, folderName);
|
||||||
|
_logger.Trace("Changing path for {0} to {1}", s.Title, s.Path);
|
||||||
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.Trace("Not changing path for: {0}", s.Title);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.Debug("Updating {0} series", series.Count);
|
||||||
_seriesRepository.UpdateMany(series);
|
_seriesRepository.UpdateMany(series);
|
||||||
|
_logger.Debug("{0} series updated", series.Count);
|
||||||
|
|
||||||
return series;
|
return series;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
using System;
|
||||||
|
using FluentValidation.Validators;
|
||||||
|
using NzbDrone.Core.Tv;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Validation.Paths
|
||||||
|
{
|
||||||
|
public class SeriesExistsValidator : PropertyValidator
|
||||||
|
{
|
||||||
|
private readonly ISeriesService _seriesService;
|
||||||
|
|
||||||
|
public SeriesExistsValidator(ISeriesService seriesService)
|
||||||
|
: base("This series has already been added")
|
||||||
|
{
|
||||||
|
_seriesService = seriesService;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool IsValid(PropertyValidatorContext context)
|
||||||
|
{
|
||||||
|
if (context.PropertyValue == null) return true;
|
||||||
|
|
||||||
|
var tvdbId = Convert.ToInt32(context.PropertyValue.ToString());
|
||||||
|
|
||||||
|
return (!_seriesService.GetAllSeries().Exists(s => s.TvdbId == tvdbId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
using FluentValidation.Validators;
|
||||||
|
using NzbDrone.Common;
|
||||||
|
using NzbDrone.Core.Tv;
|
||||||
|
using Omu.ValueInjecter;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Validation.Paths
|
||||||
|
{
|
||||||
|
public class SeriesPathValidator : PropertyValidator
|
||||||
|
{
|
||||||
|
private readonly ISeriesService _seriesService;
|
||||||
|
|
||||||
|
public SeriesPathValidator(ISeriesService seriesService)
|
||||||
|
: base("Path is already configured for another series")
|
||||||
|
{
|
||||||
|
_seriesService = seriesService;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool IsValid(PropertyValidatorContext context)
|
||||||
|
{
|
||||||
|
if (context.PropertyValue == null) return true;
|
||||||
|
|
||||||
|
var series = new Series();
|
||||||
|
series.InjectFrom(context.ParentContext.InstanceToValidate);
|
||||||
|
|
||||||
|
if (series.Id == 0) return true;
|
||||||
|
|
||||||
|
return (!_seriesService.GetAllSeries().Exists(s => s.Path.PathEquals(context.PropertyValue.ToString()) && s.Id != series.Id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,10 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
define(
|
define(
|
||||||
[
|
[
|
||||||
|
'underscore',
|
||||||
'vent',
|
'vent',
|
||||||
'AppLayout',
|
'AppLayout',
|
||||||
'underscore',
|
'backbone',
|
||||||
'marionette',
|
'marionette',
|
||||||
'Quality/QualityProfileCollection',
|
'Quality/QualityProfileCollection',
|
||||||
'AddSeries/RootFolders/RootFolderCollection',
|
'AddSeries/RootFolders/RootFolderCollection',
|
||||||
|
@ -13,7 +14,18 @@ define(
|
||||||
'Shared/Messenger',
|
'Shared/Messenger',
|
||||||
'Mixins/AsValidatedView',
|
'Mixins/AsValidatedView',
|
||||||
'jquery.dotdotdot'
|
'jquery.dotdotdot'
|
||||||
], function (vent, AppLayout, _, Marionette, QualityProfiles, RootFolders, RootFolderLayout, SeriesCollection, Config, Messenger, AsValidatedView) {
|
], function (_,
|
||||||
|
vent,
|
||||||
|
AppLayout,
|
||||||
|
Backbone,
|
||||||
|
Marionette,
|
||||||
|
QualityProfiles,
|
||||||
|
RootFolders,
|
||||||
|
RootFolderLayout,
|
||||||
|
SeriesCollection,
|
||||||
|
Config,
|
||||||
|
Messenger,
|
||||||
|
AsValidatedView) {
|
||||||
|
|
||||||
var view = Marionette.ItemView.extend({
|
var view = Marionette.ItemView.extend({
|
||||||
|
|
||||||
|
@ -156,7 +168,17 @@ define(
|
||||||
icon.removeClass('icon-spin icon-spinner disabled').addClass('icon-search');
|
icon.removeClass('icon-spin icon-spinner disabled').addClass('icon-search');
|
||||||
|
|
||||||
Messenger.show({
|
Messenger.show({
|
||||||
message: 'Added: ' + self.model.get('title')
|
message: 'Added: ' + self.model.get('title'),
|
||||||
|
actions : {
|
||||||
|
goToSeries: {
|
||||||
|
label: 'Go to Series Page',
|
||||||
|
action: function() {
|
||||||
|
Backbone.history.navigate('/series/' + self.model.get('titleSlug'), { trigger: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hideAfter: 8,
|
||||||
|
hideOnNavigate: true
|
||||||
});
|
});
|
||||||
|
|
||||||
vent.trigger(vent.Events.SeriesAdded, { series: self.model });
|
vent.trigger(vent.Events.SeriesAdded, { series: self.model });
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
{{overview}}
|
{{overview}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{{#unless existing}}
|
||||||
<div class="row labels">
|
<div class="row labels">
|
||||||
{{#unless path}}
|
{{#unless path}}
|
||||||
<div class="span4">Path</div>
|
<div class="span4">Path</div>
|
||||||
|
@ -29,6 +30,7 @@
|
||||||
<div class="span1 starting-season starting-season-label">Starting Season</div>
|
<div class="span1 starting-season starting-season-label">Starting Season</div>
|
||||||
<div class="span2">Quality Profile</div>
|
<div class="span2">Quality Profile</div>
|
||||||
</div>
|
</div>
|
||||||
|
{{/unless}}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<form class="form-inline">
|
<form class="form-inline">
|
||||||
{{#if existing}}
|
{{#if existing}}
|
||||||
|
|
|
@ -76,6 +76,10 @@ define(
|
||||||
size : 14,
|
size : 14,
|
||||||
animate : false
|
animate : false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.$(element).find('.chart').tooltip({
|
||||||
|
title: 'Episode is downloading - {0}% {1}'.format(event.progress.toFixed(1), event.releaseTitle)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -109,6 +113,7 @@ define(
|
||||||
allDay : false,
|
allDay : false,
|
||||||
statusLevel : self._getStatusLevel(model, end),
|
statusLevel : self._getStatusLevel(model, end),
|
||||||
progress : self._getDownloadProgress(model),
|
progress : self._getDownloadProgress(model),
|
||||||
|
releaseTitle: self._getReleaseTitle(model),
|
||||||
model : model
|
model : model
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -163,6 +168,16 @@ define(
|
||||||
}
|
}
|
||||||
|
|
||||||
return 100 - (downloading.get('sizeleft') / downloading.get('size') * 100);
|
return 100 - (downloading.get('sizeleft') / downloading.get('size') * 100);
|
||||||
|
},
|
||||||
|
|
||||||
|
_getReleaseTitle: function (element) {
|
||||||
|
var downloading = QueueCollection.findEpisode(element.get('id'));
|
||||||
|
|
||||||
|
if (!downloading) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return downloading.get('title');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -22,7 +22,7 @@ define(
|
||||||
_onClick: function () {
|
_onClick: function () {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
if (window.confirm('Are you sure you want to delete \'{0}\' form disk?'.format(this.model.get('path')))) {
|
if (window.confirm('Are you sure you want to delete \'{0}\' from disk?'.format(this.model.get('path')))) {
|
||||||
this.model.destroy()
|
this.model.destroy()
|
||||||
.done(function () {
|
.done(function () {
|
||||||
vent.trigger(vent.Events.EpisodeFileDeleted, { episodeFile: self.model });
|
vent.trigger(vent.Events.EpisodeFileDeleted, { episodeFile: self.model });
|
||||||
|
|
|
@ -69,7 +69,7 @@ define(
|
||||||
if (downloading) {
|
if (downloading) {
|
||||||
var progress = 100 - (downloading.get('sizeleft') / downloading.get('size') * 100);
|
var progress = 100 - (downloading.get('sizeleft') / downloading.get('size') * 100);
|
||||||
|
|
||||||
this.$el.html('<div class="progress progress-purple" title="Episode is downloading - {0}%" data-container="body">'.format(progress.toFixed(1)) +
|
this.$el.html('<div class="progress progress-purple" title="Episode is downloading - {0}% {1}" data-container="body">'.format(progress.toFixed(1), downloading.get('title')) +
|
||||||
'<div class="bar" style="width: {0}%;"></div></div>'.format(progress));
|
'<div class="bar" style="width: {0}%;"></div></div>'.format(progress));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,3 +41,7 @@
|
||||||
height : 1em;
|
height : 1em;
|
||||||
line-height : 1em;
|
line-height : 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tooltip-inner {
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
define(
|
||||||
|
[
|
||||||
|
'jquery',
|
||||||
|
'vent',
|
||||||
|
'marionette',
|
||||||
|
'Cells/NzbDroneCell'
|
||||||
|
], function ($, vent, Marionette, NzbDroneCell) {
|
||||||
|
return NzbDroneCell.extend({
|
||||||
|
|
||||||
|
className: 'episode-actions-cell',
|
||||||
|
|
||||||
|
events: {
|
||||||
|
'click .x-failed' : '_markAsFailed'
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function () {
|
||||||
|
this.$el.empty();
|
||||||
|
|
||||||
|
if (this.model.get('eventType') === 'grabbed') {
|
||||||
|
this.$el.html('<i class="icon-nd-delete x-failed" title="Mark download as failed"></i>');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
_markAsFailed: function () {
|
||||||
|
var url = window.NzbDrone.ApiRoot + '/history/failed';
|
||||||
|
var data = {
|
||||||
|
id: this.model.get('id')
|
||||||
|
};
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: url,
|
||||||
|
type: 'POST',
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
|
@ -7,9 +7,18 @@ define(
|
||||||
'Cells/EventTypeCell',
|
'Cells/EventTypeCell',
|
||||||
'Cells/QualityCell',
|
'Cells/QualityCell',
|
||||||
'Cells/RelativeDateCell',
|
'Cells/RelativeDateCell',
|
||||||
|
'Episode/Activity/EpisodeActivityActionsCell',
|
||||||
'Episode/Activity/NoActivityView',
|
'Episode/Activity/NoActivityView',
|
||||||
'Shared/LoadingView'
|
'Shared/LoadingView'
|
||||||
], function (Marionette, Backgrid, HistoryCollection, EventTypeCell, QualityCell, RelativeDateCell, NoActivityView, LoadingView) {
|
], function (Marionette,
|
||||||
|
Backgrid,
|
||||||
|
HistoryCollection,
|
||||||
|
EventTypeCell,
|
||||||
|
QualityCell,
|
||||||
|
RelativeDateCell,
|
||||||
|
EpisodeActivityActionsCell,
|
||||||
|
NoActivityView,
|
||||||
|
LoadingView) {
|
||||||
|
|
||||||
return Marionette.Layout.extend({
|
return Marionette.Layout.extend({
|
||||||
template: 'Episode/Activity/EpisodeActivityLayoutTemplate',
|
template: 'Episode/Activity/EpisodeActivityLayoutTemplate',
|
||||||
|
@ -40,6 +49,12 @@ define(
|
||||||
name : 'date',
|
name : 'date',
|
||||||
label: 'Date',
|
label: 'Date',
|
||||||
cell : RelativeDateCell
|
cell : RelativeDateCell
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'this',
|
||||||
|
label : '',
|
||||||
|
cell : EpisodeActivityActionsCell,
|
||||||
|
sortable: false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ define(
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
data: data
|
data: data
|
||||||
});
|
});
|
||||||
|
|
||||||
vent.trigger(vent.Commands.CloseModalCommand);
|
vent.trigger(vent.Commands.CloseModalCommand);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,42 +13,58 @@
|
||||||
{{#if_eq eventType compare="grabbed"}}
|
{{#if_eq eventType compare="grabbed"}}
|
||||||
<dl class="dl-horizontal">
|
<dl class="dl-horizontal">
|
||||||
|
|
||||||
<dt>Name</dt>
|
<dt>Name:</dt>
|
||||||
<dd>{{sourceTitle}}</dd>
|
<dd>{{sourceTitle}}</dd>
|
||||||
|
|
||||||
{{#with data}}
|
{{#with data}}
|
||||||
{{#if indexer}}
|
{{#if indexer}}
|
||||||
<dt>Indexer</dt>
|
<dt>Indexer:</dt>
|
||||||
<dd>{{indexer}}</dd>
|
<dd>{{indexer}}</dd>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if releaseGroup}}
|
{{#if releaseGroup}}
|
||||||
<dt>Release Group</dt>
|
<dt>Release Group:</dt>
|
||||||
<dd>{{releaseGroup}}</dd>
|
<dd>{{releaseGroup}}</dd>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if nzbInfoUrl}}
|
{{#if nzbInfoUrl}}
|
||||||
<dt>Info</dt>
|
<dt>Info:</dt>
|
||||||
<dd><a href="{{nzbInfoUrl}}">{{nzbInfoUrl}}</a></dd>
|
<dd><a href="{{nzbInfoUrl}}">{{nzbInfoUrl}}</a></dd>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if downloadClient}}
|
||||||
|
<dt>Download Client:</dt>
|
||||||
|
<dd>{{downloadClient}}</dd>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if downloadClientId}}
|
||||||
|
<dt>Download Client ID:</dt>
|
||||||
|
<dd>{{downloadClientId}}</dd>
|
||||||
|
{{/if}}
|
||||||
{{/with}}
|
{{/with}}
|
||||||
</dl>
|
</dl>
|
||||||
{{/if_eq}}
|
{{/if_eq}}
|
||||||
{{#if_eq eventType compare="downloadFailed"}}
|
{{#if_eq eventType compare="downloadFailed"}}
|
||||||
<dl class="dl-horizontal">
|
<dl class="dl-horizontal">
|
||||||
<dt>Source Title</dt>
|
|
||||||
|
<dt>Name:</dt>
|
||||||
<dd>{{sourceTitle}}</dd>
|
<dd>{{sourceTitle}}</dd>
|
||||||
|
|
||||||
{{#with data}}
|
{{#with data}}
|
||||||
<dt>Message</dt>
|
<dt>Message:</dt>
|
||||||
<dd>{{message}}</dd>
|
<dd>{{message}}</dd>
|
||||||
{{/with}}
|
{{/with}}
|
||||||
</dl>
|
</dl>
|
||||||
{{/if_eq}}
|
{{/if_eq}}
|
||||||
{{#if_eq eventType compare="downloadFolderImported"}}
|
{{#if_eq eventType compare="downloadFolderImported"}}
|
||||||
{{#if data}}
|
<dl class="dl-horizontal">
|
||||||
|
|
||||||
|
{{#if sourceTitle}}
|
||||||
|
<dt>Name:</dt>
|
||||||
|
<dd>{{sourceTitle}}</dd>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
{{#with data}}
|
{{#with data}}
|
||||||
<dl class="dl-horizontal">
|
|
||||||
{{#if droppedPath}}
|
{{#if droppedPath}}
|
||||||
<dt>Source:</dt>
|
<dt>Source:</dt>
|
||||||
<dd>{{droppedPath}}</dd>
|
<dd>{{droppedPath}}</dd>
|
||||||
|
@ -58,11 +74,8 @@
|
||||||
<dt>Imported To:</dt>
|
<dt>Imported To:</dt>
|
||||||
<dd>{{importedPath}}</dd>
|
<dd>{{importedPath}}</dd>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</dl>
|
|
||||||
{{/with}}
|
{{/with}}
|
||||||
{{else}}
|
</dl>
|
||||||
No details available
|
|
||||||
{{/if}}
|
|
||||||
{{/if_eq}}
|
{{/if_eq}}
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
|
|
|
@ -22,3 +22,30 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
{{#if_mono}}
|
||||||
|
<fieldset class="advanced-setting">
|
||||||
|
<legend>Importing</legend>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label class="control-label">Skip Free Space Check</label>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<label class="checkbox toggle well">
|
||||||
|
<input type="checkbox" name="skipFreeSpaceCheckWhenImporting"/>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<span>Yes</span>
|
||||||
|
<span>No</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="btn btn-primary slide-button"/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<span class="help-inline-checkbox">
|
||||||
|
<i class="icon-nd-form-info" title="Use when drone is unable to detect free space from your series root folder"/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
{{/if_mono}}
|
||||||
|
|
|
@ -23,13 +23,16 @@ define(function () {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
options.hideOnNavigate = options.hideOnNavigate || false;
|
||||||
|
|
||||||
return window.Messenger().post({
|
return window.Messenger().post({
|
||||||
message : options.message,
|
message : options.message,
|
||||||
type : options.type,
|
type : options.type,
|
||||||
showCloseButton: true,
|
showCloseButton: true,
|
||||||
hideAfter : options.hideAfter,
|
hideAfter : options.hideAfter,
|
||||||
id : options.id,
|
id : options.id,
|
||||||
actions : options.actions
|
actions : options.actions,
|
||||||
|
hideOnNavigate : options.hideOnNavigate
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ define(
|
||||||
], function (Backbone, StatusModel) {
|
], function (Backbone, StatusModel) {
|
||||||
return Backbone.Model.extend({
|
return Backbone.Model.extend({
|
||||||
url: function () {
|
url: function () {
|
||||||
return StatusModel.get('urlBase') + '/logfile/' + this.get('filename');
|
return StatusModel.get('urlBase') + '/api/log/file/' + this.get('filename');
|
||||||
},
|
},
|
||||||
|
|
||||||
parse: function (contents) {
|
parse: function (contents) {
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
define(
|
||||||
|
[
|
||||||
|
'marionette'
|
||||||
|
], function (Marionette) {
|
||||||
|
return Marionette.ItemView.extend({
|
||||||
|
template: 'System/Update/EmptyViewTemplate'
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1 @@
|
||||||
|
<div>No updates are available</div>
|
|
@ -2,9 +2,11 @@
|
||||||
define(
|
define(
|
||||||
[
|
[
|
||||||
'marionette',
|
'marionette',
|
||||||
'System/Update/UpdateItemView'
|
'System/Update/UpdateItemView',
|
||||||
], function (Marionette, UpdateItemView) {
|
'System/Update/EmptyView'
|
||||||
|
], function (Marionette, UpdateItemView, EmptyView) {
|
||||||
return Marionette.CollectionView.extend({
|
return Marionette.CollectionView.extend({
|
||||||
itemView: UpdateItemView
|
itemView : UpdateItemView,
|
||||||
|
emptyView: EmptyView
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue