Merge branch 'develop'

This commit is contained in:
Mark McDowall 2014-04-08 17:21:29 -07:00
commit 878a89b4e2
67 changed files with 1215 additions and 291 deletions

View File

@ -127,6 +127,9 @@ Function PackageOsx()
Write-Host "Adding sqlite dylibs"
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']"
}

View File

@ -184,6 +184,9 @@ namespace Exceptron.Client
{
report.cul = Thread.CurrentThread.CurrentCulture.Name;
if (string.IsNullOrEmpty(report.cul))
report.cul = "en";
try
{
report.os = Environment.OSVersion.VersionString;

Binary file not shown.

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<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="freebsd" dll="MediaInfo.dll" target="libmediainfo.so.0" />
<dllmap os="solaris" dll="MediaInfo.dll" target="libmediainfo.so.0.0.0" />

View File

@ -12,12 +12,10 @@ namespace NzbDrone.Api.Authentication
{
public class EnableStatelessAuthInNancy : IRegisterNancyPipeline
{
private readonly IAuthenticationService _authenticationService;
private static String API_KEY;
public EnableStatelessAuthInNancy(IAuthenticationService authenticationService, IConfigFileProvider configFileProvider)
public EnableStatelessAuthInNancy(IConfigFileProvider configFileProvider)
{
_authenticationService = authenticationService;
API_KEY = configFileProvider.ApiKey;
}
@ -30,16 +28,11 @@ namespace NzbDrone.Api.Authentication
{
Response response = null;
if (!RuntimeInfo.IsProduction && context.Request.IsLocalRequest())
{
return response;
}
var authorizationHeader = context.Request.Headers.Authorization;
var apiKeyHeader = context.Request.Headers["X-Api-Key"].FirstOrDefault();
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 };
}
@ -49,15 +42,9 @@ namespace NzbDrone.Api.Authentication
private bool ValidApiKey(string apiKey)
{
if (apiKey.IsNullOrWhiteSpace()) return false;
if (!apiKey.Equals(API_KEY)) return false;
if (!API_KEY.Equals(apiKey)) return false;
return true;
}
private bool IsAuthenticated(NancyContext context)
{
return _authenticationService.Enabled && _authenticationService.IsAuthenticated(context);
}
}
}

View File

@ -40,8 +40,8 @@ namespace NzbDrone.Api.Calendar
var occurrence = icalCalendar.Create<Event>();
occurrence.UID = "NzbDrone_episode_" + episode.Id.ToString();
occurrence.Status = episode.HasFile ? EventStatus.Confirmed : EventStatus.Tentative;
occurrence.Start = new iCalDateTime(episode.AirDateUtc.Value);
occurrence.End = new iCalDateTime(episode.AirDateUtc.Value.AddMinutes(episode.Series.Runtime));
occurrence.Start = new iCalDateTime(episode.AirDateUtc.Value) { HasTime = true };
occurrence.End = new iCalDateTime(episode.AirDateUtc.Value.AddMinutes(episode.Series.Runtime)) { HasTime = true };
occurrence.Description = episode.Overview;
occurrence.Categories = new List<string>() { episode.Series.Network };

View File

@ -17,5 +17,7 @@ namespace NzbDrone.Api.Config
public String FolderChmod { get; set; }
public String ChownUser { get; set; }
public String ChownGroup { get; set; }
public Boolean SkipFreeSpaceCheckWhenImporting { get; set; }
}
}

View File

@ -7,7 +7,7 @@ namespace NzbDrone.Api.Extensions
{
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)
@ -21,11 +21,5 @@ namespace NzbDrone.Api.Extensions
request.UserHostAddress.Equals("127.0.0.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);
}
}
}

View File

@ -4,11 +4,15 @@ using System.Linq;
using NzbDrone.Common;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using Nancy;
using Nancy.Responses;
namespace NzbDrone.Api.Logs
{
public class LogFileModule : NzbDroneRestModule<LogFileResource>
{
private const string LOGFILE_ROUTE = @"/(?<filename>nzbdrone(?:\.\d+)?\.txt)";
private readonly IAppFolderInfo _appFolderInfo;
private readonly IDiskProvider _diskProvider;
@ -19,6 +23,8 @@ namespace NzbDrone.Api.Logs
_appFolderInfo = appFolderInfo;
_diskProvider = diskProvider;
GetResourceAll = GetLogFiles;
Get[LOGFILE_ROUTE] = options => GetLogFile(options.filename);
}
private List<LogFileResource> GetLogFiles()
@ -41,5 +47,17 @@ namespace NzbDrone.Api.Logs
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);
}
}
}

View File

@ -32,7 +32,13 @@ namespace NzbDrone.Api.Series
public SeriesModule(ICommandExecutor commandExecutor,
ISeriesService seriesService,
ISeriesStatisticsService seriesStatisticsService,
IMapCoversToLocal coverMapper)
IMapCoversToLocal coverMapper,
RootFolderValidator rootFolderValidator,
PathExistsValidator pathExistsValidator,
SeriesPathValidator seriesPathValidator,
SeriesExistsValidator seriesExistsValidator,
DroneFactoryValidator droneFactoryValidator
)
: base(commandExecutor)
{
_commandExecutor = commandExecutor;
@ -48,11 +54,18 @@ namespace NzbDrone.Api.Series
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.RootFolderPath).IsValidPath().When(s => String.IsNullOrEmpty(s.Path));
PostValidator.RuleFor(s => s.Title).NotEmpty();
PostValidator.RuleFor(s => s.TvdbId).GreaterThan(0).SetValidator(seriesExistsValidator);
}
private SeriesResource GetSeries(int id)

View File

@ -13,6 +13,7 @@ namespace NzbDrone.Common
{
string DownloadString(string url);
string DownloadString(string url, string username, string password);
string DownloadString(string url, ICredentials credentials);
Dictionary<string, string> GetHeader(string url);
Stream DownloadStream(string url, NetworkCredential credential = null);
@ -44,7 +45,7 @@ namespace NzbDrone.Common
return DownloadString(url, new NetworkCredential(username, password));
}
private string DownloadString(string url, ICredentials identity)
public string DownloadString(string url, ICredentials identity)
{
try
{

View File

@ -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
{
@ -163,7 +163,7 @@ namespace NzbDrone.Common.Processes
path = "mono";
}
Logger.Info("Starting {0} {1}", path, args);
Logger.Debug("Starting {0} {1}", path, args);
var startInfo = new ProcessStartInfo(path, args);
var process = new Process

View File

@ -10,6 +10,7 @@ using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
using NzbDrone.Test.Common;
using FizzWare.NBuilder;
namespace NzbDrone.Core.Test.DecisionEngineTests
{
@ -141,7 +142,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
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);
_reports[0].Title = "1937 - Snow White and the Seven Dwarves";
@ -204,5 +206,49 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -12,6 +12,7 @@ using NzbDrone.Core.MediaFiles.Commands;
using NzbDrone.Core.MediaFiles.EpisodeImport;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
using NzbDrone.Test.Common;
@ -22,7 +23,7 @@ namespace NzbDrone.Core.Test.MediaFiles
public class DownloadedEpisodesImportServiceFixture : CoreTest<DownloadedEpisodesImportService>
{
private string[] _subFolders = new[] { "c:\\root\\foldername".AsOsAgnostic() };
private string[] _videoFiles = new[] { "c:\\root\\foldername\\video.ext".AsOsAgnostic() };
private string[] _videoFiles = new[] { "c:\\root\\foldername\\30.rock.s01e01.ext".AsOsAgnostic() };
[SetUp]
public void Setup()
@ -113,6 +114,8 @@ namespace NzbDrone.Core.Test.MediaFiles
Mocker.GetMock<IParsingService>()
.Verify(v => v.GetSeries(It.IsAny<String>()), Times.Never());
ExceptionVerification.ExpectedWarns(1);
}
[Test]
@ -129,7 +132,7 @@ namespace NzbDrone.Core.Test.MediaFiles
}
[Test]
public void should_delete_folder_if_files_were_imported()
public void should_delete_folder_if_files_were_imported_and_video_files_remain()
{
GivenValidSeries();
@ -148,6 +151,40 @@ namespace NzbDrone.Core.Test.MediaFiles
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>()
.Verify(v => v.DeleteFolder(It.IsAny<String>(), true), Times.Once());
}

View File

@ -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();
}
}
}

View File

@ -7,6 +7,7 @@ using Moq;
using NUnit.Framework;
using NzbDrone.Common;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles.EpisodeImport.Specifications;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
@ -143,5 +144,15 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
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();
}
}
}

View File

@ -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]
public void should_return_true_for_existing_file()
{
_localEpisode.ExistingFile = true;
Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue();
}
[Test]
public void should_return_true_for_flv()
{
_localEpisode.Path = @"C:\Test\some.show.s01e01.flv";
Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue();
Mocker.GetMock<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();
}
}
}

View File

@ -96,7 +96,9 @@ namespace NzbDrone.Core.Test.MetadataSourceTests
private void ValidateEpisode(Episode episode)
{
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();

View File

@ -138,6 +138,7 @@
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedMetadataFilesFixture.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupDuplicateMetadataFilesFixture.cs" />
<Compile Include="Housekeeping\Housekeepers\FixFutureRunScheduledTasksFixture.cs" />
<Compile Include="IndexerSearchTests\NzbSearchServiceFixture.cs" />
<Compile Include="IndexerSearchTests\SearchDefinitionFixture.cs" />
<Compile Include="IndexerTests\BasicRssParserFixture.cs" />
<Compile Include="IndexerTests\IndexerServiceFixture.cs" />
@ -155,6 +156,7 @@
<Compile Include="MediaFiles\EpisodeImport\Specifications\FreeSpaceSpecificationFixture.cs" />
<Compile Include="MediaFiles\EpisodeImport\ImportDecisionMakerFixture.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\NotUnpackingSpecificationFixture.cs" />
<Compile Include="MediaFiles\EpisodeImport\Specifications\UpgradeSpecificationFixture.cs" />
@ -212,6 +214,7 @@
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedEpisodesFixture.cs" />
<Compile Include="ThingiProviderTests\NullConfigFixture.cs" />
<Compile Include="ThingiProvider\ProviderBaseFixture.cs" />
<Compile Include="TvTests\EpisodeRepositoryTests\ByAirDateFixture.cs" />
<Compile Include="TvTests\EpisodeRepositoryTests\EpisodesWithFilesFixture.cs" />
<Compile Include="TvTests\EpisodeRepositoryTests\EpisodesWhereCutoffUnmetFixture.cs" />
<Compile Include="TvTests\RefreshEpisodeServiceFixture.cs" />

View File

@ -39,6 +39,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Ray Donovan - S01E01.720p.HDtv.x264-Evolve (NLsub)", Language.Dutch)]
[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("The.Trip.To.Italy.S02E01.720p.HDTV.x264-TLA", Language.English)]
public void should_parse_language(string postTitle, Language language)
{
var result = Parser.Parser.ParseTitle(postTitle);

View File

@ -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();
}
}
}

View File

@ -164,6 +164,13 @@ namespace NzbDrone.Core.Configuration
set { SetValue("DownloadedEpisodesScanInterval", value); }
}
public Boolean SkipFreeSpaceCheckWhenImporting
{
get { return GetValueBoolean("SkipFreeSpaceCheckWhenImporting", false); }
set { SetValue("SkipFreeSpaceCheckWhenImporting", value); }
}
public Boolean SetPermissionsLinux
{
get { return GetValueBoolean("SetPermissionsLinux", false); }

View File

@ -26,6 +26,7 @@ namespace NzbDrone.Core.Configuration
Boolean AutoDownloadPropers { get; set; }
Boolean CreateEmptySeriesFolders { get; set; }
FileDateType FileDate { get; set; }
Boolean SkipFreeSpaceCheckWhenImporting { get; set; }
//Permissions (Media Management)
Boolean SetPermissionsLinux { get; set; }

View File

@ -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;
}
}
}

View File

@ -4,7 +4,6 @@ using System.Linq;
using NLog;
using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Download
{

View File

@ -3,7 +3,7 @@ using NzbDrone.Core.Messaging.Commands;
namespace NzbDrone.Core.IndexerSearch
{
public class MissingEpisodeSearchCommand : Command
public class EpisodeSearchCommand : Command
{
public List<int> EpisodeIds { get; set; }

View File

@ -50,7 +50,7 @@ namespace NzbDrone.Core.IndexerSearch
PageSize = 100000,
SortDirection = SortDirection.Ascending,
SortKey = "Id",
FilterExpression = v => v.Monitored && v.Series.Monitored
FilterExpression = v => v.Monitored == true && v.Series.Monitored == true
}).Records.ToList();
_logger.ProgressInfo("Performing missing search for {0} episodes", episodes.Count);

View File

@ -3,7 +3,7 @@ using NzbDrone.Core.Messaging.Commands;
namespace NzbDrone.Core.IndexerSearch
{
public class EpisodeSearchCommand : Command
public class MissingEpisodeSearchCommand : Command
{
public List<int> EpisodeIds { get; set; }

View File

@ -138,10 +138,53 @@ namespace NzbDrone.Core.IndexerSearch
return SearchSpecial(series, episodes);
}
var searchSpec = Get<SeasonSearchCriteria>(series, episodes);
searchSpec.SeasonNumber = seasonNumber;
List<DownloadDecision> downloadDecisions = new List<DownloadDecision>();
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()

View File

@ -1,3 +1,4 @@
using System;
using NzbDrone.Core.Messaging.Commands;
namespace NzbDrone.Core.MediaFiles.Commands
@ -12,6 +13,7 @@ namespace NzbDrone.Core.MediaFiles.Commands
}
}
public bool SendUpdates { get; set; }
public Boolean SendUpdates { get; set; }
public String Path { get; set; }
}
}

View File

@ -1,10 +1,12 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using NLog;
using NzbDrone.Common;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnsureThat;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles.Commands;
using NzbDrone.Core.MediaFiles.EpisodeImport;
@ -24,6 +26,7 @@ namespace NzbDrone.Core.MediaFiles
private readonly IConfigService _configService;
private readonly IMakeImportDecision _importDecisionMaker;
private readonly IImportApprovedEpisodes _importApprovedEpisodes;
private readonly ISampleService _sampleService;
private readonly Logger _logger;
public DownloadedEpisodesImportService(IDiskProvider diskProvider,
@ -33,6 +36,7 @@ namespace NzbDrone.Core.MediaFiles
IConfigService configService,
IMakeImportDecision importDecisionMaker,
IImportApprovedEpisodes importApprovedEpisodes,
ISampleService sampleService,
Logger logger)
{
_diskProvider = diskProvider;
@ -42,6 +46,7 @@ namespace NzbDrone.Core.MediaFiles
_configService = configService;
_importDecisionMaker = importDecisionMaker;
_importApprovedEpisodes = importApprovedEpisodes;
_sampleService = sampleService;
_logger = logger;
}
@ -64,24 +69,7 @@ namespace NzbDrone.Core.MediaFiles
foreach (var subFolder in _diskProvider.GetDirectories(downloadedEpisodesFolder))
{
try
{
if (_seriesService.SeriesPathExists(subFolder))
{
continue;
}
var importedFiles = ProcessSubFolder(new DirectoryInfo(subFolder));
if (importedFiles.Any())
{
_diskProvider.DeleteFolder(subFolder, true);
}
}
catch (Exception e)
{
_logger.ErrorException("An error has occurred while importing folder: " + subFolder, e);
}
ProcessFolder(subFolder);
}
foreach (var videoFile in _diskScanService.GetVideoFiles(downloadedEpisodesFolder, false))
@ -97,9 +85,9 @@ namespace NzbDrone.Core.MediaFiles
}
}
private List<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 quality = QualityParser.ParseQuality(cleanedUpName);
_logger.Debug("{0} folder quality: {1}", cleanedUpName, quality);
@ -110,7 +98,7 @@ namespace NzbDrone.Core.MediaFiles
return new List<ImportDecision>();
}
var videoFiles = _diskScanService.GetVideoFiles(subfolderInfo.FullName);
var videoFiles = _diskScanService.GetVideoFiles(directoryInfo.FullName);
return ProcessFiles(series, quality, videoFiles);
}
@ -140,6 +128,33 @@ namespace NzbDrone.Core.MediaFiles
return _importApprovedEpisodes.Import(decisions, true);
}
private void ProcessFolder(string path)
{
Ensure.That(path, () => path).IsValidPath();
try
{
if (_seriesService.SeriesPathExists(path))
{
_logger.Warn("Unable to process folder that contains sorted TV Shows");
return;
}
var directoryFolderInfo = new DirectoryInfo(path);
var importedFiles = ProcessFolder(directoryFolderInfo);
if (importedFiles.Any() && ShouldDeleteFolder(directoryFolderInfo))
{
_logger.Debug("Deleting folder after importing valid files");
_diskProvider.DeleteFolder(path, true);
}
}
catch (Exception e)
{
_logger.ErrorException("An error has occurred while importing folder: " + path, e);
}
}
private string GetCleanedUpFolderName(string folder)
{
folder = folder.Replace("_UNPACK_", "")
@ -148,9 +163,47 @@ namespace NzbDrone.Core.MediaFiles
return folder;
}
private bool ShouldDeleteFolder(DirectoryInfo directoryInfo)
{
var videoFiles = _diskScanService.GetVideoFiles(directoryInfo.FullName);
var cleanedUpName = GetCleanedUpFolderName(directoryInfo.Name);
var series = _parsingService.GetSeries(cleanedUpName);
foreach (var videoFile in videoFiles)
{
var episodeParseResult = Parser.Parser.ParseTitle(Path.GetFileName(videoFile));
if (episodeParseResult == null)
{
_logger.Warn("Unable to parse file on import: [{0}]", videoFile);
return false;
}
var size = _diskProvider.GetFileSize(videoFile);
var quality = QualityParser.ParseQuality(videoFile);
if (!_sampleService.IsSample(series, quality, videoFile, size,
episodeParseResult.SeasonNumber))
{
_logger.Warn("Non-sample file detected: [{0}]", videoFile);
return false;
}
}
return true;
}
public void Execute(DownloadedEpisodesScanCommand message)
{
ProcessDownloadedEpisodesFolder();
if (message.Path.IsNullOrWhiteSpace())
{
ProcessDownloadedEpisodesFolder();
}
else
{
ProcessFolder(message.Path);
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -1,8 +1,8 @@
using System;
using System.IO;
using NLog;
using NzbDrone.Common;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
@ -10,11 +10,13 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
public class FreeSpaceSpecification : IImportDecisionEngineSpecification
{
private readonly IDiskProvider _diskProvider;
private readonly IConfigService _configService;
private readonly Logger _logger;
public FreeSpaceSpecification(IDiskProvider diskProvider, Logger logger)
public FreeSpaceSpecification(IDiskProvider diskProvider, IConfigService configService, Logger logger)
{
_diskProvider = diskProvider;
_configService = configService;
_logger = logger;
}
@ -22,6 +24,12 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
public bool IsSatisfiedBy(LocalEpisode localEpisode)
{
if (_configService.SkipFreeSpaceCheckWhenImporting)
{
_logger.Debug("Skipping free space check when importing");
return true;
}
try
{
if (localEpisode.ExistingFile)

View File

@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.IO;
using NLog;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.MediaFiles.MediaInfo;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Qualities;
@ -12,25 +11,16 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
{
public class NotSampleSpecification : IImportDecisionEngineSpecification
{
private readonly IVideoFileInfoReader _videoFileInfoReader;
private readonly ISampleService _sampleService;
private readonly Logger _logger;
private static List<Quality> _largeSampleSizeQualities = new List<Quality> { Quality.HDTV1080p, Quality.WEBDL1080p, Quality.Bluray1080p };
public NotSampleSpecification(IVideoFileInfoReader videoFileInfoReader,
public NotSampleSpecification(ISampleService sampleService,
Logger logger)
{
_videoFileInfoReader = videoFileInfoReader;
_sampleService = sampleService;
_logger = logger;
}
public static long SampleSizeLimit
{
get
{
return 70.Megabytes();
}
}
public string RejectionReason { get { return "Sample"; } }
public bool IsSatisfiedBy(LocalEpisode localEpisode)
@ -41,72 +31,11 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
return true;
}
if (localEpisode.Series.SeriesType == SeriesTypes.Daily)
{
_logger.Debug("Daily Series, skipping sample check");
return true;
}
if (localEpisode.SeasonNumber == 0)
{
_logger.Debug("Special, skipping sample check");
return true;
}
var extension = Path.GetExtension(localEpisode.Path);
if (extension != null && extension.Equals(".flv", StringComparison.InvariantCultureIgnoreCase))
{
_logger.Debug("Skipping sample check for .flv file");
return true;
}
try
{
var runTime = _videoFileInfoReader.GetRunTime(localEpisode.Path);
if (runTime.TotalMinutes.Equals(0))
{
_logger.Error("[{0}] has a runtime of 0, is it a valid video file?", localEpisode);
return false;
}
if (runTime.TotalSeconds < 90)
{
_logger.Debug("[{0}] appears to be a sample. Size: {1} Runtime: {2}", localEpisode.Path, localEpisode.Size, runTime);
return false;
}
}
catch (DllNotFoundException)
{
_logger.Debug("Falling back to file size detection");
return CheckSize(localEpisode);
}
_logger.Debug("Runtime is over 90 seconds");
return true;
}
private bool CheckSize(LocalEpisode localEpisode)
{
if (_largeSampleSizeQualities.Contains(localEpisode.Quality.Quality))
{
if (localEpisode.Size < SampleSizeLimit * 2)
{
_logger.Debug("1080p file is less than sample limit");
return false;
}
}
if (localEpisode.Size < SampleSizeLimit)
{
_logger.Debug("File is less than sample limit");
return false;
}
return true;
return !_sampleService.IsSample(localEpisode.Series,
localEpisode.Quality,
localEpisode.Path,
localEpisode.Size,
localEpisode.SeasonNumber);
}
}
}

View File

@ -193,16 +193,22 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc
{
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;
}
else if (Int32.TryParse(seasonNumberMatch, out seasonNumber))
{
metadata.SeasonNumber = seasonNumber;
}
else
{
metadata.SeasonNumber = Convert.ToInt32(seasonNumber);
return null;
}
return metadata;
@ -313,14 +319,16 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc
}
_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
{
SeriesId = series.Id,
Consumer = GetType().Name,
Type = MetadataType.SeriesImage,
RelativePath = DiskProviderBase.GetRelativePath(series.Path, destination)
RelativePath = relativePath
};
yield return metadata;
@ -341,18 +349,20 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc
}
var path = Path.Combine(series.Path, filename);
var relativePath = DiskProviderBase.GetRelativePath(series.Path, path);
DownloadImage(series, image.Url, path);
var metadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.SeasonImage &&
c.SeasonNumber == season.SeasonNumber) ??
c.SeasonNumber == season.SeasonNumber &&
c.RelativePath == relativePath) ??
new MetadataFile
{
SeriesId = series.Id,
SeasonNumber = season.SeasonNumber,
Consumer = GetType().Name,
Type = MetadataType.SeasonImage,
RelativePath = DiskProviderBase.GetRelativePath(series.Path, path)
RelativePath = relativePath
};
yield return metadata;
@ -458,7 +468,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc
var filename = GetEpisodeImageFilename(episodeFile.Path);
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);
if (existingMetadata != null)

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Growl.Connector;
using NLog;
using NzbDrone.Common.Instrumentation;
@ -63,6 +64,8 @@ namespace NzbDrone.Core.Notifications.Growl
const string title = "Test Notification";
const string body = "This is a test message from NzbDrone";
Thread.Sleep(5000);
SendNotification(title, body, "TEST", message.Host, message.Port, message.Password);
}
}

View File

@ -30,7 +30,13 @@ namespace NzbDrone.Core.Notifications.Plex
[FieldDefinition(1, Label = "Port")]
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 bool IsValid

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Xml.Linq;
using NLog;
using NzbDrone.Common;
@ -58,7 +59,7 @@ namespace NzbDrone.Core.Notifications.Plex
{
_logger.Debug("Getting sections from Plex host: {0}", settings.Host);
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 mediaContainer = xDoc.Descendants("MediaContainer").FirstOrDefault();
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);
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)
@ -85,6 +86,13 @@ namespace NzbDrone.Core.Notifications.Plex
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)
{
_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)
{
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");
}

View File

@ -14,5 +14,7 @@ namespace NzbDrone.Core.Notifications.Plex
public string Host { get; set; }
public int Port { get; set; }
public string Username { get; set; }
public string Password { get; set; }
}
}

View File

@ -11,7 +11,6 @@ namespace NzbDrone.Core.Notifications.PushBullet
public PushBulletSettingsValidator()
{
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/")]
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 bool IsValid

View File

@ -20,14 +20,14 @@ namespace NzbDrone.Core.Notifications.Pushover
{
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)
{
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)

View File

@ -1,4 +1,5 @@
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Common;
using NzbDrone.Core.Messaging.Commands;
using RestSharp;
using NzbDrone.Core.Rest;
@ -6,14 +7,14 @@ namespace NzbDrone.Core.Notifications.Pushover
{
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>
{
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 request = new RestRequest(Method.POST);
@ -23,6 +24,9 @@ namespace NzbDrone.Core.Notifications.Pushover
request.AddParameter("message", message);
request.AddParameter("priority", (int)priority);
if (!sound.IsNullOrWhiteSpace()) request.AddParameter("sound", sound);
client.ExecuteAndValidate(request);
}
@ -31,7 +35,7 @@ namespace NzbDrone.Core.Notifications.Pushover
const string title = "Test Notification";
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);
}
}
}

View File

@ -27,6 +27,9 @@ namespace NzbDrone.Core.Notifications.Pushover
[FieldDefinition(2, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(PushoverPriority) )]
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
{
get

View File

@ -16,5 +16,6 @@ namespace NzbDrone.Core.Notifications.Pushover
public string ApiKey { get; set; }
public string UserKey { get; set; }
public int Priority { get; set; }
public string Sound { get; set; }
}
}

View File

@ -217,6 +217,7 @@
<Compile Include="DecisionEngine\Specifications\NotRestrictedReleaseSpecification.cs" />
<Compile Include="DecisionEngine\Specifications\NotSampleSpecification.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\SeasonMatchSpecification.cs" />
<Compile Include="DecisionEngine\Specifications\Search\DailyEpisodeMatchSpecification.cs" />
@ -326,6 +327,7 @@
<Compile Include="MediaCover\MediaCoversUpdatedEvent.cs" />
<Compile Include="MediaFiles\Commands\RenameFilesCommand.cs" />
<Compile Include="MediaFiles\EpisodeFileMoveResult.cs" />
<Compile Include="MediaFiles\EpisodeImport\SampleService.cs" />
<Compile Include="MediaFiles\EpisodeImport\Specifications\FullSeasonSpecification.cs" />
<Compile Include="MediaFiles\Events\SeriesScannedEvent.cs" />
<Compile Include="MediaFiles\FileDateType.cs" />
@ -680,9 +682,11 @@
<Compile Include="Update\UpdatePackageProvider.cs" />
<Compile Include="Update\UpdatePackage.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\PathValidator.cs" />
<Compile Include="Validation\Paths\RootFolderValidator.cs" />
<Compile Include="Validation\Paths\SeriesPathValidator.cs" />
<Compile Include="Validation\Paths\PathExistsValidator.cs" />
<Compile Include="Validation\FolderValidator.cs" />
<Compile Include="Validation\RuleBuilderExtensions.cs" />

View File

@ -107,7 +107,7 @@ namespace NzbDrone.Core.Parser
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);
private static readonly Regex YearInTitleRegex = new Regex(@"^(?<title>.+?)(?:\W|_)?(?<year>\d{4})",

View File

@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Marr.Data.QGen;
using NLog;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Datastore.Extentions;
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 absoluteEpisodeNumber);
Episode Get(int seriesId, String date);
Episode Find(int seriesId, String date);
Episode Get(int seriesId, string date);
Episode Find(int seriesId, string date);
List<Episode> GetEpisodes(int seriesId);
List<Episode> GetEpisodes(int seriesId, int seasonNumber);
List<Episode> GetEpisodeByFileId(int fileId);
@ -32,11 +34,13 @@ namespace NzbDrone.Core.Tv
public class EpisodeRepository : BasicRepository<Episode>, IEpisodeRepository
{
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)
{
_database = database;
_logger = logger;
}
public Episode Find(int seriesId, int season, int episodeNumber)
@ -54,18 +58,21 @@ namespace NzbDrone.Core.Tv
.SingleOrDefault();
}
public Episode Get(int seriesId, String date)
public Episode Get(int seriesId, string date)
{
return Query.Where(s => s.SeriesId == seriesId)
.AndWhere(s => s.AirDate == date)
.Single();
var episode = FindOneByAirDate(seriesId, date);
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)
.AndWhere(s => s.AirDate == date)
.SingleOrDefault();
return FindOneByAirDate(seriesId, date);
}
public List<Episode> GetEpisodes(int seriesId)
@ -207,5 +214,28 @@ namespace NzbDrone.Core.Tv
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");
}
}
}

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using NLog;
using NzbDrone.Common;
using NzbDrone.Common.EnsureThat;
using NzbDrone.Core.DataAugmentation.Scene;
using NzbDrone.Core.Messaging.Events;
@ -197,14 +198,22 @@ namespace NzbDrone.Core.Tv
{
foreach (var s in series)
{
if (!String.IsNullOrWhiteSpace(s.RootFolderPath))
if (!s.RootFolderPath.IsNullOrWhiteSpace())
{
var folderName = new DirectoryInfo(s.Path).Name;
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);
_logger.Debug("{0} series updated", series.Count);
return series;
}

View File

@ -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));
}
}
}

View File

@ -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));
}
}
}

View File

@ -1,9 +1,10 @@
'use strict';
define(
[
'underscore',
'vent',
'AppLayout',
'underscore',
'backbone',
'marionette',
'Quality/QualityProfileCollection',
'AddSeries/RootFolders/RootFolderCollection',
@ -13,7 +14,18 @@ define(
'Shared/Messenger',
'Mixins/AsValidatedView',
'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({
@ -156,7 +168,17 @@ define(
icon.removeClass('icon-spin icon-spinner disabled').addClass('icon-search');
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 });

View File

@ -21,6 +21,7 @@
{{overview}}
</div>
</div>
{{#unless existing}}
<div class="row labels">
{{#unless path}}
<div class="span4">Path</div>
@ -29,6 +30,7 @@
<div class="span1 starting-season starting-season-label">Starting Season</div>
<div class="span2">Quality Profile</div>
</div>
{{/unless}}
<div class="row">
<form class="form-inline">
{{#if existing}}

View File

@ -76,6 +76,10 @@ define(
size : 14,
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,
statusLevel : self._getStatusLevel(model, end),
progress : self._getDownloadProgress(model),
releaseTitle: self._getReleaseTitle(model),
model : model
};
@ -163,6 +168,16 @@ define(
}
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');
}
});
});

View File

@ -22,7 +22,7 @@ define(
_onClick: function () {
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()
.done(function () {
vent.trigger(vent.Events.EpisodeFileDeleted, { episodeFile: self.model });

View File

@ -69,7 +69,7 @@ define(
if (downloading) {
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));
return;
}

View File

@ -41,3 +41,7 @@
height : 1em;
line-height : 1em;
}
.tooltip-inner {
word-wrap: break-word;
}

View File

@ -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
});
}
});
});

View File

@ -7,9 +7,18 @@ define(
'Cells/EventTypeCell',
'Cells/QualityCell',
'Cells/RelativeDateCell',
'Episode/Activity/EpisodeActivityActionsCell',
'Episode/Activity/NoActivityView',
'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({
template: 'Episode/Activity/EpisodeActivityLayoutTemplate',
@ -40,6 +49,12 @@ define(
name : 'date',
label: 'Date',
cell : RelativeDateCell
},
{
name : 'this',
label : '',
cell : EpisodeActivityActionsCell,
sortable: false
}
],

View File

@ -24,6 +24,7 @@ define(
type: 'POST',
data: data
});
vent.trigger(vent.Commands.CloseModalCommand);
}
});

View File

@ -13,42 +13,58 @@
{{#if_eq eventType compare="grabbed"}}
<dl class="dl-horizontal">
<dt>Name</dt>
<dt>Name:</dt>
<dd>{{sourceTitle}}</dd>
{{#with data}}
{{#if indexer}}
<dt>Indexer</dt>
<dt>Indexer:</dt>
<dd>{{indexer}}</dd>
{{/if}}
{{#if releaseGroup}}
<dt>Release Group</dt>
<dt>Release Group:</dt>
<dd>{{releaseGroup}}</dd>
{{/if}}
{{#if nzbInfoUrl}}
<dt>Info</dt>
<dt>Info:</dt>
<dd><a href="{{nzbInfoUrl}}">{{nzbInfoUrl}}</a></dd>
{{/if}}
{{#if downloadClient}}
<dt>Download Client:</dt>
<dd>{{downloadClient}}</dd>
{{/if}}
{{#if downloadClientId}}
<dt>Download Client ID:</dt>
<dd>{{downloadClientId}}</dd>
{{/if}}
{{/with}}
</dl>
{{/if_eq}}
{{#if_eq eventType compare="downloadFailed"}}
<dl class="dl-horizontal">
<dt>Source Title</dt>
<dt>Name:</dt>
<dd>{{sourceTitle}}</dd>
{{#with data}}
<dt>Message</dt>
<dt>Message:</dt>
<dd>{{message}}</dd>
{{/with}}
</dl>
{{/if_eq}}
{{#if_eq eventType compare="downloadFolderImported"}}
{{#if data}}
<dl class="dl-horizontal">
{{#if sourceTitle}}
<dt>Name:</dt>
<dd>{{sourceTitle}}</dd>
{{/if}}
{{#with data}}
<dl class="dl-horizontal">
{{#if droppedPath}}
<dt>Source:</dt>
<dd>{{droppedPath}}</dd>
@ -58,11 +74,8 @@
<dt>Imported To:</dt>
<dd>{{importedPath}}</dd>
{{/if}}
</dl>
{{/with}}
{{else}}
No details available
{{/if}}
</dl>
{{/if_eq}}
</div>
<div class="modal-footer">

View File

@ -22,3 +22,30 @@
</div>
</div>
</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}}

View File

@ -23,13 +23,16 @@ define(function () {
}
}
options.hideOnNavigate = options.hideOnNavigate || false;
return window.Messenger().post({
message : options.message,
type : options.type,
showCloseButton: true,
hideAfter : options.hideAfter,
id : options.id,
actions : options.actions
actions : options.actions,
hideOnNavigate : options.hideOnNavigate
});
},

View File

@ -6,7 +6,7 @@ define(
], function (Backbone, StatusModel) {
return Backbone.Model.extend({
url: function () {
return StatusModel.get('urlBase') + '/logfile/' + this.get('filename');
return StatusModel.get('urlBase') + '/api/log/file/' + this.get('filename');
},
parse: function (contents) {

View File

@ -0,0 +1,10 @@
'use strict';
define(
[
'marionette'
], function (Marionette) {
return Marionette.ItemView.extend({
template: 'System/Update/EmptyViewTemplate'
});
});

View File

@ -0,0 +1 @@
<div>No updates are available</div>

View File

@ -2,9 +2,11 @@
define(
[
'marionette',
'System/Update/UpdateItemView'
], function (Marionette, UpdateItemView) {
'System/Update/UpdateItemView',
'System/Update/EmptyView'
], function (Marionette, UpdateItemView, EmptyView) {
return Marionette.CollectionView.extend({
itemView: UpdateItemView
itemView : UpdateItemView,
emptyView: EmptyView
});
});