Merge branch 'develop'

This commit is contained in:
Mark McDowall 2014-05-06 15:58:54 -07:00
commit fd81356097
48 changed files with 639 additions and 68 deletions

View File

@ -5,7 +5,6 @@ using Nancy.Bootstrapper;
using NzbDrone.Api.Extensions; using NzbDrone.Api.Extensions;
using NzbDrone.Api.Extensions.Pipelines; using NzbDrone.Api.Extensions.Pipelines;
using NzbDrone.Common; using NzbDrone.Common;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
namespace NzbDrone.Api.Authentication namespace NzbDrone.Api.Authentication
@ -28,11 +27,9 @@ namespace NzbDrone.Api.Authentication
{ {
Response response = null; Response response = null;
var authorizationHeader = context.Request.Headers.Authorization; var apiKey = GetApiKey(context);
var apiKeyHeader = context.Request.Headers["X-Api-Key"].FirstOrDefault();
var apiKey = apiKeyHeader.IsNullOrWhiteSpace() ? authorizationHeader : apiKeyHeader;
if (context.Request.IsApiRequest() && !ValidApiKey(apiKey)) if ((context.Request.IsApiRequest() || context.Request.IsFeedRequest()) && !ValidApiKey(apiKey))
{ {
response = new Response { StatusCode = HttpStatusCode.Unauthorized }; response = new Response { StatusCode = HttpStatusCode.Unauthorized };
} }
@ -46,5 +43,23 @@ namespace NzbDrone.Api.Authentication
return true; return true;
} }
private string GetApiKey(NancyContext context)
{
var apiKeyHeader = context.Request.Headers["X-Api-Key"].FirstOrDefault();
var apiKeyQueryString = context.Request.Query["ApiKey"];
if (!apiKeyHeader.IsNullOrWhiteSpace())
{
return apiKeyHeader;
}
if (apiKeyQueryString.HasValue)
{
return apiKeyQueryString.Value;
}
return context.Request.Headers.Authorization;
}
} }
} }

View File

@ -1,7 +1,9 @@
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using FluentValidation; using FluentValidation;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Validation;
using Omu.ValueInjecter; using Omu.ValueInjecter;
namespace NzbDrone.Api.Config namespace NzbDrone.Api.Config
@ -25,8 +27,8 @@ namespace NzbDrone.Api.Config
SharedValidator.RuleFor(c => c.Username).NotEmpty().When(c => c.AuthenticationEnabled); SharedValidator.RuleFor(c => c.Username).NotEmpty().When(c => c.AuthenticationEnabled);
SharedValidator.RuleFor(c => c.Password).NotEmpty().When(c => c.AuthenticationEnabled); SharedValidator.RuleFor(c => c.Password).NotEmpty().When(c => c.AuthenticationEnabled);
SharedValidator.RuleFor(c => c.SslPort).InclusiveBetween(1, 65535).When(c => c.EnableSsl); SharedValidator.RuleFor(c => c.SslPort).ValidPort().When(c => c.EnableSsl);
SharedValidator.RuleFor(c => c.SslCertHash).NotEmpty().When(c => c.EnableSsl); SharedValidator.RuleFor(c => c.SslCertHash).NotEmpty().When(c => c.EnableSsl && OsInfo.IsWindows);
} }
private HostConfigResource GetHostConfig() private HostConfigResource GetHostConfig()

View File

@ -10,6 +10,11 @@ namespace NzbDrone.Api.Extensions
return request.Path.StartsWith("/api/", StringComparison.InvariantCultureIgnoreCase); return request.Path.StartsWith("/api/", StringComparison.InvariantCultureIgnoreCase);
} }
public static bool IsFeedRequest(this Request request)
{
return request.Path.StartsWith("/feed/", StringComparison.InvariantCultureIgnoreCase);
}
public static bool IsSignalRRequest(this Request request) public static bool IsSignalRRequest(this Request request)
{ {
return request.Path.StartsWith("/signalr/", StringComparison.InvariantCultureIgnoreCase); return request.Path.StartsWith("/signalr/", StringComparison.InvariantCultureIgnoreCase);

View File

@ -5,7 +5,7 @@ using NzbDrone.Api.REST;
using NzbDrone.Api.Series; using NzbDrone.Api.Series;
using NzbDrone.Core.History; using NzbDrone.Core.History;
using NzbDrone.Core.Qualities; using NzbDrone.Core.Qualities;
using NzbDrone.Core.Tv;
namespace NzbDrone.Api.History namespace NzbDrone.Api.History
{ {

View File

@ -1,7 +1,6 @@
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using FluentValidation; using FluentValidation;
using FluentValidation.Validators; using FluentValidation.Validators;
using NzbDrone.Core.Validation.Paths;
namespace NzbDrone.Api.Validation namespace NzbDrone.Api.Validation
{ {

View File

@ -1,9 +1,6 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.AccessControl; using System.Security.AccessControl;
using System.Security.Principal; using System.Security.Principal;
using System.Text;
using NLog; using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.Instrumentation; using NzbDrone.Common.Instrumentation;

View File

@ -103,6 +103,7 @@
<Compile Include="Messaging\IMessage.cs" /> <Compile Include="Messaging\IMessage.cs" />
<Compile Include="PathEqualityComparer.cs" /> <Compile Include="PathEqualityComparer.cs" />
<Compile Include="Processes\INzbDroneProcessProvider.cs" /> <Compile Include="Processes\INzbDroneProcessProvider.cs" />
<Compile Include="Processes\PidFileProvider.cs" />
<Compile Include="Processes\ProcessOutput.cs" /> <Compile Include="Processes\ProcessOutput.cs" />
<Compile Include="RateGate.cs" /> <Compile Include="RateGate.cs" />
<Compile Include="Serializer\IntConverter.cs" /> <Compile Include="Serializer\IntConverter.cs" />

View File

@ -0,0 +1,44 @@
using System;
using System.IO;
using NLog;
using NzbDrone.Common.EnvironmentInfo;
namespace NzbDrone.Common.Processes
{
public interface IProvidePidFile
{
void Write();
}
public class PidFileProvider : IProvidePidFile
{
private readonly IAppFolderInfo _appFolderInfo;
private readonly IProcessProvider _processProvider;
private readonly Logger _logger;
public PidFileProvider(IAppFolderInfo appFolderInfo, IProcessProvider processProvider, Logger logger)
{
_appFolderInfo = appFolderInfo;
_processProvider = processProvider;
_logger = logger;
}
public void Write()
{
var filename = Path.Combine(_appFolderInfo.AppDataFolder, "nzbdrone.pid");
if (OsInfo.IsMono)
{
try
{
File.WriteAllText(filename, _processProvider.GetCurrentProcess().Id.ToString());
}
catch (Exception ex)
{
_logger.Error("Unable to write PID file: " + filename, ex);
throw;
}
}
}
}
}

View File

@ -48,6 +48,14 @@ namespace NzbDrone.Common
return stringBuilder.ToString().Normalize(NormalizationForm.FormC); return stringBuilder.ToString().Normalize(NormalizationForm.FormC);
} }
public static string TrimEnd(this string text, string postfix)
{
if (text.EndsWith(postfix))
text = text.Substring(0, text.Length - postfix.Length);
return text;
}
public static string CleanSpaces(this string text) public static string CleanSpaces(this string text)
{ {
return CollapseSpace.Replace(text, " ").Trim(); return CollapseSpace.Replace(text, " ").Trim();

View File

@ -66,5 +66,113 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
Subject.Clean(); Subject.Clean();
AllStoredModels.Count.Should().Be(1); AllStoredModels.Count.Should().Be(1);
} }
[Test]
public void should_not_delete_metadata_files_when_they_are_for_the_same_episode_but_different_consumers()
{
var files = Builder<MetadataFile>.CreateListOfSize(2)
.All()
.With(m => m.Type = MetadataType.EpisodeMetadata)
.With(m => m.EpisodeFileId = 1)
.BuildListOfNew();
Db.InsertMany(files);
Subject.Clean();
AllStoredModels.Count.Should().Be(files.Count);
}
[Test]
public void should_not_delete_metadata_files_for_different_episode()
{
var files = Builder<MetadataFile>.CreateListOfSize(2)
.All()
.With(m => m.Type = MetadataType.EpisodeMetadata)
.With(m => m.Consumer = "XbmcMetadata")
.BuildListOfNew();
Db.InsertMany(files);
Subject.Clean();
AllStoredModels.Count.Should().Be(files.Count);
}
[Test]
public void should_delete_metadata_files_when_they_are_for_the_same_episode_and_consumer()
{
var files = Builder<MetadataFile>.CreateListOfSize(2)
.All()
.With(m => m.Type = MetadataType.EpisodeMetadata)
.With(m => m.EpisodeFileId = 1)
.With(m => m.Consumer = "XbmcMetadata")
.BuildListOfNew();
Db.InsertMany(files);
Subject.Clean();
AllStoredModels.Count.Should().Be(1);
}
[Test]
public void should_not_delete_metadata_files_when_there_is_only_one_for_that_episode_and_consumer()
{
var file = Builder<MetadataFile>.CreateNew()
.BuildNew();
Db.Insert(file);
Subject.Clean();
AllStoredModels.Count.Should().Be(1);
}
[Test]
public void should_not_delete_image_when_they_are_for_the_same_episode_but_different_consumers()
{
var files = Builder<MetadataFile>.CreateListOfSize(2)
.All()
.With(m => m.Type = MetadataType.EpisodeImage)
.With(m => m.EpisodeFileId = 1)
.BuildListOfNew();
Db.InsertMany(files);
Subject.Clean();
AllStoredModels.Count.Should().Be(files.Count);
}
[Test]
public void should_not_delete_image_for_different_episode()
{
var files = Builder<MetadataFile>.CreateListOfSize(2)
.All()
.With(m => m.Type = MetadataType.EpisodeImage)
.With(m => m.Consumer = "XbmcMetadata")
.BuildListOfNew();
Db.InsertMany(files);
Subject.Clean();
AllStoredModels.Count.Should().Be(files.Count);
}
[Test]
public void should_delete_image_when_they_are_for_the_same_episode_and_consumer()
{
var files = Builder<MetadataFile>.CreateListOfSize(2)
.All()
.With(m => m.Type = MetadataType.EpisodeImage)
.With(m => m.EpisodeFileId = 1)
.With(m => m.Consumer = "XbmcMetadata")
.BuildListOfNew();
Db.InsertMany(files);
Subject.Clean();
AllStoredModels.Count.Should().Be(1);
}
[Test]
public void should_not_delete_image_when_there_is_only_one_for_that_episode_and_consumer()
{
var file = Builder<MetadataFile>.CreateNew()
.BuildNew();
Db.Insert(file);
Subject.Clean();
AllStoredModels.Count.Should().Be(1);
}
} }
} }

View File

@ -3,6 +3,7 @@ using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Core.Housekeeping.Housekeepers; using NzbDrone.Core.Housekeeping.Housekeepers;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Metadata;
using NzbDrone.Core.Metadata.Files; using NzbDrone.Core.Metadata.Files;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
@ -81,5 +82,43 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
Subject.Clean(); Subject.Clean();
AllStoredModels.Should().HaveCount(1); AllStoredModels.Should().HaveCount(1);
} }
[Test]
public void should_delete_episode_metadata_files_that_have_episodefileid_of_zero()
{
var series = Builder<Series>.CreateNew()
.BuildNew();
Db.Insert(series);
var metadataFile = Builder<MetadataFile>.CreateNew()
.With(m => m.SeriesId = series.Id)
.With(m => m.Type = MetadataType.EpisodeMetadata)
.With(m => m.EpisodeFileId = 0)
.BuildNew();
Db.Insert(metadataFile);
Subject.Clean();
AllStoredModels.Should().HaveCount(0);
}
[Test]
public void should_delete_episode_image_files_that_have_episodefileid_of_zero()
{
var series = Builder<Series>.CreateNew()
.BuildNew();
Db.Insert(series);
var metadataFile = Builder<MetadataFile>.CreateNew()
.With(m => m.SeriesId = series.Id)
.With(m => m.Type = MetadataType.EpisodeImage)
.With(m => m.EpisodeFileId = 0)
.BuildNew();
Db.Insert(metadataFile);
Subject.Clean();
AllStoredModels.Should().HaveCount(0);
}
} }
} }

View File

@ -405,5 +405,35 @@ namespace NzbDrone.Core.Test.OrganizerTests
Subject.BuildFilename(new List<Episode> { episode }, new Series { Title = "30 Rock" }, _episodeFile) Subject.BuildFilename(new List<Episode> { episode }, new Series { Title = "30 Rock" }, _episodeFile)
.Should().Be("30 Rock - S06E06 - Part 1"); .Should().Be("30 Rock - S06E06 - Part 1");
} }
[Test]
public void should_replace_double_period_with_single_period()
{
_namingConfig.StandardEpisodeFormat = "{Series.Title}.S{season:00}E{episode:00}.{Episode.Title}";
var episode = Builder<Episode>.CreateNew()
.With(e => e.Title = "Part 1")
.With(e => e.SeasonNumber = 6)
.With(e => e.EpisodeNumber = 6)
.Build();
Subject.BuildFilename(new List<Episode> { episode }, new Series { Title = "Chicago P.D." }, _episodeFile)
.Should().Be("Chicago.P.D.S06E06.Part.1");
}
[Test]
public void should_replace_triple_period_with_single_period()
{
_namingConfig.StandardEpisodeFormat = "{Series.Title}.S{season:00}E{episode:00}.{Episode.Title}";
var episode = Builder<Episode>.CreateNew()
.With(e => e.Title = "Part 1")
.With(e => e.SeasonNumber = 6)
.With(e => e.EpisodeNumber = 6)
.Build();
Subject.BuildFilename(new List<Episode> { episode }, new Series { Title = "Chicago P.D.." }, _episodeFile)
.Should().Be("Chicago.P.D.S06E06.Part.1");
}
} }
} }

View File

@ -7,6 +7,7 @@ using NzbDrone.Core.Parser;
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 System.Text;
namespace NzbDrone.Core.Test.ParserTests namespace NzbDrone.Core.Test.ParserTests
{ {
@ -29,10 +30,60 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("THIS SHOULD NEVER PARSE")] [TestCase("THIS SHOULD NEVER PARSE")]
[TestCase("Vh1FvU3bJXw6zs8EEUX4bMo5vbbMdHghxHirc.mkv")] [TestCase("Vh1FvU3bJXw6zs8EEUX4bMo5vbbMdHghxHirc.mkv")]
[TestCase("0e895c37245186812cb08aab1529cf8ee389dd05.mkv")] [TestCase("0e895c37245186812cb08aab1529cf8ee389dd05.mkv")]
[TestCase("08bbc153931ce3ca5fcafe1b92d3297285feb061.mkv")]
[TestCase("185d86a343e39f3341e35c4dad3ff159")]
public void should_not_parse_crap(string title) public void should_not_parse_crap(string title)
{ {
Parser.Parser.ParseTitle(title).Should().BeNull(); Parser.Parser.ParseTitle(title).Should().BeNull();
ExceptionVerification.IgnoreWarns(); ExceptionVerification.IgnoreWarns();
} }
[Test]
public void should_not_parse_md5()
{
string hash = "CRAPPY TEST SEED";
var hashAlgo = System.Security.Cryptography.MD5.Create();
var repetitions = 100;
var success = 0;
for (int i = 0; i < repetitions; i++)
{
var hashData = hashAlgo.ComputeHash(System.Text.Encoding.Default.GetBytes(hash));
hash = BitConverter.ToString(hashData).Replace("-", "");
if (Parser.Parser.ParseTitle(hash) == null)
success++;
}
success.Should().Be(repetitions);
}
[TestCase(32)]
[TestCase(40)]
public void should_not_parse_random(int length)
{
string charset = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
var hashAlgo = new Random();
var repetitions = 500;
var success = 0;
for (int i = 0; i < repetitions; i++)
{
StringBuilder hash = new StringBuilder(length);
for (int x = 0; x < length; x++)
{
hash.Append(charset[hashAlgo.Next() % charset.Length]);
}
if (Parser.Parser.ParseTitle(hash.ToString()) == null)
success++;
}
success.Should().Be(repetitions);
}
} }
} }

View File

@ -24,6 +24,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("2020.NZ.2012.16.02.PDTV.XviD-C4TV", "2020nz", 2012, 2, 16)] [TestCase("2020.NZ.2012.16.02.PDTV.XviD-C4TV", "2020nz", 2012, 2, 16)]
[TestCase("2020.NZ.2012.13.02.PDTV.XviD-C4TV", "2020nz", 2012, 2, 13)] [TestCase("2020.NZ.2012.13.02.PDTV.XviD-C4TV", "2020nz", 2012, 2, 13)]
[TestCase("2020.NZ.2011.12.02.PDTV.XviD-C4TV", "2020nz", 2011, 12, 2)] [TestCase("2020.NZ.2011.12.02.PDTV.XviD-C4TV", "2020nz", 2011, 12, 2)]
[TestCase("Series Title - 2013-10-30 - Episode Title (1) [HDTV-720p]", "Series Title", 2013, 10, 30)]
public void should_parse_daily_episode(string postTitle, string title, int year, int month, int day) public void should_parse_daily_episode(string postTitle, string title, int year, int month, int day)
{ {
var result = Parser.Parser.ParseTitle(postTitle); var result = Parser.Parser.ParseTitle(postTitle);

View File

@ -12,17 +12,31 @@ namespace NzbDrone.Core.Test.ParserTests
{ {
new object[] new object[]
{ {
@"C:\Test\Some.Hashed.Release.S01E01.720p.WEB-DL.AAC2.0.H.264-Mercury\0e895c3724.mkv".AsOsAgnostic(), @"C:\Test\Some.Hashed.Release.S01E01.720p.WEB-DL.AAC2.0.H.264-Mercury\0e895c37245186812cb08aab1529cf8ee389dd05.mkv".AsOsAgnostic(),
"somehashedrelease", "somehashedrelease",
"WEBDL-720p", "WEBDL-720p",
"Mercury" "Mercury"
}, },
new object[] new object[]
{ {
@"C:\Test\0e895c3724\Some.Hashed.Release.S01E01.720p.WEB-DL.AAC2.0.H.264-Mercury.mkv".AsOsAgnostic(), @"C:\Test\0e895c37245186812cb08aab1529cf8ee389dd05\Some.Hashed.Release.S01E01.720p.WEB-DL.AAC2.0.H.264-Mercury.mkv".AsOsAgnostic(),
"somehashedrelease", "somehashedrelease",
"WEBDL-720p", "WEBDL-720p",
"Mercury" "Mercury"
},
new object[]
{
@"C:\Test\Some.Hashed.Release.S01E01.720p.WEB-DL.AAC2.0.H.264-Mercury.mkv\yrucreM-462.H.0.2CAA.LD-BEW.p027.10E10S.esaeleR.dehsaH.emoS.mkv".AsOsAgnostic(),
"somehashedrelease",
"WEBDL-720p",
"Mercury"
},
new object[]
{
@"C:\Test\Weeds.S01E10.DVDRip.XviD-NZBgeek\AHFMZXGHEWD660.mkv".AsOsAgnostic(),
"weeds",
"DVD",
"NZBgeek"
} }
}; };

View File

@ -24,11 +24,17 @@ namespace NzbDrone.Core.Test.ParserTests
} }
[Test] [Test]
public void should_not_include_extension_in_release_roup() public void should_not_include_extension_in_release_group()
{ {
const string path = @"C:\Test\Doctor.Who.2005.s01e01.internal.bdrip.x264-archivist.mkv"; const string path = @"C:\Test\Doctor.Who.2005.s01e01.internal.bdrip.x264-archivist.mkv";
Parser.Parser.ParsePath(path).ReleaseGroup.Should().Be("archivist"); Parser.Parser.ParsePath(path).ReleaseGroup.Should().Be("archivist");
} }
[TestCase("The.Longest.Mystery.S02E04.720p.WEB-DL.AAC2.0.H.264-EVL-RP", "EVL")]
public void should_not_include_repost_in_release_group(string title, string expected)
{
Parser.Parser.ParseReleaseGroup(title).Should().Be(expected);
}
} }
} }

View File

@ -0,0 +1,16 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(49)]
public class fix_dognzb_url : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Execute.Sql("UPDATE Indexers SET Settings = replace(Settings, '//dognzb.cr', '//api.dognzb.cr')" +
"WHERE Implementation = 'Newznab'" +
"AND Settings LIKE '%//dognzb.cr%'");
}
}
}

View File

@ -19,6 +19,8 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
_logger.Debug("Running cleanup of duplicate metadata files"); _logger.Debug("Running cleanup of duplicate metadata files");
DeleteDuplicateSeriesMetadata(); DeleteDuplicateSeriesMetadata();
DeleteDuplicateEpisodeMetadata();
DeleteDuplicateEpisodeImages();
} }
private void DeleteDuplicateSeriesMetadata() private void DeleteDuplicateSeriesMetadata()
@ -33,5 +35,31 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
HAVING COUNT(SeriesId) > 1 HAVING COUNT(SeriesId) > 1
)"); )");
} }
private void DeleteDuplicateEpisodeMetadata()
{
var mapper = _database.GetDataMapper();
mapper.ExecuteNonQuery(@"DELETE FROM MetadataFiles
WHERE Id IN (
SELECT Id FROM MetadataFiles
WHERE Type = 2
GROUP BY EpisodeFileId, Consumer
HAVING COUNT(EpisodeFileId) > 1
)");
}
private void DeleteDuplicateEpisodeImages()
{
var mapper = _database.GetDataMapper();
mapper.ExecuteNonQuery(@"DELETE FROM MetadataFiles
WHERE Id IN (
SELECT Id FROM MetadataFiles
WHERE Type = 5
GROUP BY EpisodeFileId, Consumer
HAVING COUNT(EpisodeFileId) > 1
)");
}
} }
} }

View File

@ -20,6 +20,7 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
DeleteOrphanedBySeries(); DeleteOrphanedBySeries();
DeleteOrphanedByEpisodeFile(); DeleteOrphanedByEpisodeFile();
DeleteWhereEpisodeFileIsZero();
} }
private void DeleteOrphanedBySeries() private void DeleteOrphanedBySeries()
@ -46,5 +47,16 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
WHERE MetadataFiles.EpisodeFileId > 0 WHERE MetadataFiles.EpisodeFileId > 0
AND EpisodeFiles.Id IS NULL)"); AND EpisodeFiles.Id IS NULL)");
} }
private void DeleteWhereEpisodeFileIsZero()
{
var mapper = _database.GetDataMapper();
mapper.ExecuteNonQuery(@"DELETE FROM MetadataFiles
WHERE Id IN (
SELECT Id FROM MetadataFiles
WHERE Type IN (2, 5)
AND EpisodeFileId = 0)");
}
} }
} }

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using NLog; using NLog;
using NzbDrone.Common; using NzbDrone.Common;
@ -13,7 +14,8 @@ namespace NzbDrone.Core.IndexerSearch
{ {
public interface IEpisodeSearchService public interface IEpisodeSearchService
{ {
void MissingEpisodesAiredAfter(DateTime dateTime); void MissingEpisodesAiredAfter(DateTime dateTime, IEnumerable<Int32> grabbed
);
} }
public class MissingEpisodeSearchService : IEpisodeSearchService, IExecute<EpisodeSearchCommand>, IExecute<MissingEpisodeSearchCommand> public class MissingEpisodeSearchService : IEpisodeSearchService, IExecute<EpisodeSearchCommand>, IExecute<MissingEpisodeSearchCommand>
@ -37,11 +39,12 @@ namespace NzbDrone.Core.IndexerSearch
_logger = logger; _logger = logger;
} }
public void MissingEpisodesAiredAfter(DateTime dateTime) public void MissingEpisodesAiredAfter(DateTime dateTime, IEnumerable<Int32> grabbed)
{ {
var missing = _episodeService.EpisodesBetweenDates(dateTime, DateTime.UtcNow) var missing = _episodeService.EpisodesBetweenDates(dateTime, DateTime.UtcNow)
.Where(e => !e.HasFile && .Where(e => !e.HasFile &&
!_queueService.GetQueue().Select(q => q.Episode.Id).Contains(e.Id)) !_queueService.GetQueue().Select(q => q.Episode.Id).Contains(e.Id) &&
!grabbed.Contains(e.Id))
.ToList(); .ToList();
var downloadedCount = 0; var downloadedCount = 0;

View File

@ -43,7 +43,7 @@ namespace NzbDrone.Core.Indexers.Newznab
Enable = false, Enable = false,
Name = "Dognzb.cr", Name = "Dognzb.cr",
Implementation = GetType().Name, Implementation = GetType().Name,
Settings = GetSettings("https://dognzb.cr", new List<Int32>()) Settings = GetSettings("https://api.dognzb.cr", new List<Int32>())
}); });
list.Add(new IndexerDefinition list.Add(new IndexerDefinition

View File

@ -1,7 +1,9 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using NLog; using NLog;
using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.Download; using NzbDrone.Core.Download;
using NzbDrone.Core.IndexerSearch; using NzbDrone.Core.IndexerSearch;
using NzbDrone.Core.Instrumentation.Extensions; using NzbDrone.Core.Instrumentation.Extensions;
@ -11,7 +13,7 @@ namespace NzbDrone.Core.Indexers
{ {
public interface IRssSyncService public interface IRssSyncService
{ {
void Sync(); List<DownloadDecision> Sync();
} }
public class RssSyncService : IRssSyncService, IExecute<RssSyncCommand> public class RssSyncService : IRssSyncService, IExecute<RssSyncCommand>
@ -36,7 +38,7 @@ namespace NzbDrone.Core.Indexers
} }
public void Sync() public List<DownloadDecision> Sync()
{ {
_logger.ProgressInfo("Starting RSS Sync"); _logger.ProgressInfo("Starting RSS Sync");
@ -45,16 +47,18 @@ namespace NzbDrone.Core.Indexers
var downloaded = _downloadApprovedReports.DownloadApproved(decisions); var downloaded = _downloadApprovedReports.DownloadApproved(decisions);
_logger.ProgressInfo("RSS Sync Completed. Reports found: {0}, Reports downloaded: {1}", reports.Count, downloaded.Count()); _logger.ProgressInfo("RSS Sync Completed. Reports found: {0}, Reports downloaded: {1}", reports.Count, downloaded.Count());
return downloaded;
} }
public void Execute(RssSyncCommand message) public void Execute(RssSyncCommand message)
{ {
Sync(); var downloaded = Sync();
if (message.LastExecutionTime.HasValue && DateTime.UtcNow.Subtract(message.LastExecutionTime.Value).TotalHours > 3) if (message.LastExecutionTime.HasValue && DateTime.UtcNow.Subtract(message.LastExecutionTime.Value).TotalHours > 3)
{ {
_logger.Info("RSS Sync hasn't run since: {0}. Searching for any missing episodes since then.", message.LastExecutionTime.Value); _logger.Info("RSS Sync hasn't run since: {0}. Searching for any missing episodes since then.", message.LastExecutionTime.Value);
_episodeSearchService.MissingEpisodesAiredAfter(message.LastExecutionTime.Value.AddDays(-1)); _episodeSearchService.MissingEpisodesAiredAfter(message.LastExecutionTime.Value.AddDays(-1), downloaded.SelectMany(d => d.RemoteEpisode.Episodes).Select(e => e.Id));
} }
} }
} }

View File

@ -1,4 +1,6 @@
using System.Diagnostics; using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using NLog; using NLog;
@ -76,7 +78,7 @@ namespace NzbDrone.Core.MediaFiles
} }
var videoFilesStopwatch = Stopwatch.StartNew(); var videoFilesStopwatch = Stopwatch.StartNew();
var mediaFileList = GetVideoFiles(series.Path).ToList(); var mediaFileList = GetVideoFiles(series.Path).Where(file => !file.StartsWith(Path.Combine(series.Path, "EXTRAS"))).ToList();
videoFilesStopwatch.Stop(); videoFilesStopwatch.Stop();
_logger.Trace("Finished getting episode files for: {0} [{1}]", series, videoFilesStopwatch.Elapsed); _logger.Trace("Finished getting episode files for: {0} [{1}]", series, videoFilesStopwatch.Elapsed);

View File

@ -7,6 +7,7 @@ using NzbDrone.Common;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.EnsureThat; using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Exceptions;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Organizer; using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
@ -172,15 +173,9 @@ namespace NzbDrone.Core.MediaFiles
catch (Exception ex) catch (Exception ex)
{ {
if (ex is UnauthorizedAccessException || ex is InvalidOperationException)
{ _logger.WarnException("Unable to apply permissions to: " + path, ex);
_logger.Debug("Unable to apply permissions to: ", path); _logger.DebugException(ex.Message, ex);
_logger.DebugException(ex.Message, ex);
}
else
{
throw;
}
} }
} }

View File

@ -247,6 +247,8 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc
xws.OmitXmlDeclaration = true; xws.OmitXmlDeclaration = true;
xws.Indent = false; xws.Indent = false;
var episodeGuideUrl = String.Format("http://www.thetvdb.com/api/1D62F2F90030C444/series/{0}/all/en.zip", series.TvdbId);
using (var xw = XmlWriter.Create(sb, xws)) using (var xw = XmlWriter.Create(sb, xws))
{ {
var tvShow = new XElement("tvshow"); var tvShow = new XElement("tvshow");
@ -254,10 +256,8 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc
tvShow.Add(new XElement("title", series.Title)); tvShow.Add(new XElement("title", series.Title));
tvShow.Add(new XElement("rating", (decimal)series.Ratings.Percentage/10)); tvShow.Add(new XElement("rating", (decimal)series.Ratings.Percentage/10));
tvShow.Add(new XElement("plot", series.Overview)); tvShow.Add(new XElement("plot", series.Overview));
tvShow.Add(new XElement("episodeguide", new XElement("url", episodeGuideUrl)));
//Todo: probably will need to use TVDB to use this feature... tvShow.Add(new XElement("episodeguideurl", episodeGuideUrl));
// tvShow.Add(new XElement("episodeguide", new XElement("url", episodeGuideUrl)));
// tvShow.Add(new XElement("episodeguideurl", episodeGuideUrl));
tvShow.Add(new XElement("mpaa", series.Certification)); tvShow.Add(new XElement("mpaa", series.Certification));
tvShow.Add(new XElement("id", series.TvdbId)); tvShow.Add(new XElement("id", series.TvdbId));

View File

@ -40,7 +40,10 @@ namespace NzbDrone.Core.Metadata
_logger.Debug("Looking for existing metadata in {0}", message.Series.Path); _logger.Debug("Looking for existing metadata in {0}", message.Series.Path);
var filesOnDisk = _diskProvider.GetFiles(message.Series.Path, SearchOption.AllDirectories); var filesOnDisk = _diskProvider.GetFiles(message.Series.Path, SearchOption.AllDirectories);
var possibleMetadataFiles = filesOnDisk.Where(c => !MediaFileExtensions.Extensions.Contains(Path.GetExtension(c).ToLower())).ToList();
var possibleMetadataFiles = filesOnDisk.Where(c => !MediaFileExtensions.Extensions.Contains(Path.GetExtension(c).ToLower()) &&
!c.StartsWith(Path.Combine(message.Series.Path, "EXTRAS"))).ToList();
var filteredFiles = _metadataFileService.FilterExistingFiles(possibleMetadataFiles, message.Series); var filteredFiles = _metadataFileService.FilterExistingFiles(possibleMetadataFiles, message.Series);
var metadataFiles = new List<MetadataFile>(); var metadataFiles = new List<MetadataFile>();

View File

@ -20,7 +20,7 @@ namespace NzbDrone.Core.MetadataSource
{ {
private readonly Logger _logger; private readonly Logger _logger;
private static readonly Regex CollapseSpaceRegex = new Regex(@"\s+", RegexOptions.Compiled); private static readonly Regex CollapseSpaceRegex = new Regex(@"\s+", RegexOptions.Compiled);
private static readonly Regex InvalidSearchCharRegex = new Regex(@"(?:\*|\(|\)|'|!)", RegexOptions.Compiled); private static readonly Regex InvalidSearchCharRegex = new Regex(@"(?:\*|\(|\)|'|!|@)", RegexOptions.Compiled);
public TraktProxy(Logger logger) public TraktProxy(Logger logger)
{ {
@ -168,8 +168,10 @@ namespace NzbDrone.Core.MetadataSource
phrase = phrase.RemoveAccent().ToLower(); phrase = phrase.RemoveAccent().ToLower();
phrase = InvalidSearchCharRegex.Replace(phrase, ""); phrase = InvalidSearchCharRegex.Replace(phrase, "");
phrase = CollapseSpaceRegex.Replace(phrase, " ").Trim().ToLower(); phrase = CollapseSpaceRegex.Replace(phrase, " ").Trim().ToLower();
phrase = phrase.Trim('-');
phrase = HttpUtility.UrlEncode(phrase); phrase = HttpUtility.UrlEncode(phrase);
return phrase; return phrase;
} }

View File

@ -61,7 +61,7 @@ namespace NzbDrone.Core.Notifications.Xbmc
var response = _httpProvider.PostCommand(settings.Address, settings.Username, settings.Password, postJson.ToString()); var response = _httpProvider.PostCommand(settings.Address, settings.Username, settings.Password, postJson.ToString());
Logger.Debug("Getting version from response"); Logger.Debug("Getting version from response: " + response);
var result = Json.Deserialize<XbmcJsonResult<JObject>>(response); var result = Json.Deserialize<XbmcJsonResult<JObject>>(response);
var versionObject = result.Result.Property("version"); var versionObject = result.Result.Property("version");

View File

@ -46,6 +46,8 @@ namespace NzbDrone.Core.Organizer
public static readonly Regex SeriesTitleRegex = new Regex(@"(?<token>\{(?:Series)(?<separator>\s|\.|-|_)Title\})", public static readonly Regex SeriesTitleRegex = new Regex(@"(?<token>\{(?:Series)(?<separator>\s|\.|-|_)Title\})",
RegexOptions.Compiled | RegexOptions.IgnoreCase); RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex FilenameCleanupRegex = new Regex(@"\.{2,}", RegexOptions.Compiled);
private static readonly char[] EpisodeTitleTrimCharaters = new[] { ' ', '.', '?' }; private static readonly char[] EpisodeTitleTrimCharaters = new[] { ' ', '.', '?' };
public FileNameBuilder(INamingConfigService namingConfigService, public FileNameBuilder(INamingConfigService namingConfigService,
@ -90,6 +92,7 @@ namespace NzbDrone.Core.Organizer
var sortedEpisodes = episodes.OrderBy(e => e.EpisodeNumber).ToList(); var sortedEpisodes = episodes.OrderBy(e => e.EpisodeNumber).ToList();
var pattern = namingConfig.StandardEpisodeFormat; var pattern = namingConfig.StandardEpisodeFormat;
var episodeTitles = new List<string> var episodeTitles = new List<string>
{ {
sortedEpisodes.First().Title.TrimEnd(EpisodeTitleTrimCharaters) sortedEpisodes.First().Title.TrimEnd(EpisodeTitleTrimCharaters)
@ -154,7 +157,10 @@ namespace NzbDrone.Core.Organizer
tokenValues.Add("{Episode Title}", GetEpisodeTitle(episodeTitles)); tokenValues.Add("{Episode Title}", GetEpisodeTitle(episodeTitles));
tokenValues.Add("{Quality Title}", GetQualityTitle(episodeFile.Quality)); tokenValues.Add("{Quality Title}", GetQualityTitle(episodeFile.Quality));
return CleanFilename(ReplaceTokens(pattern, tokenValues).Trim()); var filename = ReplaceTokens(pattern, tokenValues).Trim();
filename = FilenameCleanupRegex.Replace(filename, match => match.Captures[0].Value[0].ToString() );
return CleanFilename(filename);
} }
public string BuildFilePath(Series series, int seasonNumber, string fileName, string extension) public string BuildFilePath(Series series, int seasonNumber, string fileName, string extension)

View File

@ -43,7 +43,7 @@ namespace NzbDrone.Core.Parser
RegexOptions.IgnoreCase | RegexOptions.Compiled), RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Episodes without a title, Single (S01E05, 1x05) AND Multi (S01E04E05, 1x04x05, etc) //Episodes without a title, Single (S01E05, 1x05) AND Multi (S01E04E05, 1x04x05, etc)
new Regex(@"^(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]|_){1,2}(?<episode>\d{2,3}(?!\d+)))+(?![\da-z]))", new Regex(@"^(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]|_){1,2}(?<episode>\d{2,3}(?!\d+)))+)",
RegexOptions.IgnoreCase | RegexOptions.Compiled), RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Episodes with a title, Single episodes (S01E05, 1x05, etc) & Multi-episode (S01E05E06, S01E05-06, S01E05 E06, etc) //Episodes with a title, Single episodes (S01E05, 1x05, etc) & Multi-episode (S01E05E06, S01E05-06, S01E05 E06, etc)
@ -59,7 +59,7 @@ namespace NzbDrone.Core.Parser
RegexOptions.IgnoreCase | RegexOptions.Compiled), RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Anime - Title Absolute Episode Number [SubGroup] //Anime - Title Absolute Episode Number [SubGroup]
new Regex(@"^(?<title>.+?)(?:(?:_|-|\s|\.)+(?<absoluteepisode>\d{2,3}))+(?:.+?)\[(?<subgroup>.+?)\](?:\.|$)", new Regex(@"^(?<title>.+?)(?:(?:_|-|\s|\.)+(?<absoluteepisode>\d{3}(?!\d+)))+(?:.+?)\[(?<subgroup>.+?)\](?:\.|$)",
RegexOptions.IgnoreCase | RegexOptions.Compiled), RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Supports 103/113 naming //Supports 103/113 naming
@ -92,7 +92,7 @@ namespace NzbDrone.Core.Parser
RegexOptions.IgnoreCase | RegexOptions.Compiled), RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Episodes with a title, Single episodes (S01E05, 1x05, etc) & Multi-episode (S01E05E06, S01E05-06, S01E05 E06, etc) //Episodes with a title, Single episodes (S01E05, 1x05, etc) & Multi-episode (S01E05E06, S01E05-06, S01E05 E06, etc)
new Regex(@"^(?<title>.+?)(?:(\W|_)+S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]|_){1,2}(?<episode>\d{4}(?!\d+|i|p)))+(?![\da-z]))\W?(?!\\)", new Regex(@"^(?<title>.+?)(?:(\W|_)+S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]|_){1,2}(?<episode>\d{4}(?!\d+|i|p)))+)\W?(?!\\)",
RegexOptions.IgnoreCase | RegexOptions.Compiled), RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Anime - Title Absolute Episode Number //Anime - Title Absolute Episode Number
@ -100,6 +100,18 @@ namespace NzbDrone.Core.Parser
RegexOptions.IgnoreCase | RegexOptions.Compiled) RegexOptions.IgnoreCase | RegexOptions.Compiled)
}; };
private static readonly Regex[] RejectHashedReleasesRegex = new Regex[]
{
// Generic match for md5 and mixed-case hashes.
new Regex(@"^[0-9a-zA-Z]{32}", RegexOptions.Compiled),
// Format seen on some NZBGeek releases
new Regex(@"^[A-Z]{11}\d{3}$", RegexOptions.Compiled)
};
//Regex to detect whether the title was reversed.
private static readonly Regex ReversedTitleRegex = new Regex(@"\.p027\.|\.p0801\.|\.\d{2}E\d{2}S\.", RegexOptions.Compiled);
private static readonly Regex NormalizeRegex = new Regex(@"((?:\b|_)(?<!^)(a|an|the|and|or|of)(?:\b|_))|\W|_", private static readonly Regex NormalizeRegex = new Regex(@"((?:\b|_)(?<!^)(a|an|the|and|or|of)(?:\b|_))|\W|_",
RegexOptions.IgnoreCase | RegexOptions.Compiled); RegexOptions.IgnoreCase | RegexOptions.Compiled);
@ -155,6 +167,17 @@ namespace NzbDrone.Core.Parser
if (!ValidateBeforeParsing(title)) return null; if (!ValidateBeforeParsing(title)) return null;
Logger.Debug("Parsing string '{0}'", title); Logger.Debug("Parsing string '{0}'", title);
if (ReversedTitleRegex.IsMatch(title))
{
var titleWithoutExtension = RemoveFileExtension(title).ToCharArray();
Array.Reverse(titleWithoutExtension);
title = new string(titleWithoutExtension) + title.Substring(titleWithoutExtension.Length);
Logger.Debug("Reversed name detected. Converted to '{0}'", title);
}
var simpleTitle = SimpleTitleRegex.Replace(title, String.Empty); var simpleTitle = SimpleTitleRegex.Replace(title, String.Empty);
foreach (var regex in ReportTitleRegex) foreach (var regex in ReportTitleRegex)
@ -245,10 +268,9 @@ namespace NzbDrone.Core.Parser
title = title.Trim(); title = title.Trim();
if (!title.ContainsInvalidPathChars() && MediaFiles.MediaFileExtensions.Extensions.Contains(Path.GetExtension(title).ToLower())) title = RemoveFileExtension(title);
{
title = Path.GetFileNameWithoutExtension(title).Trim(); title = title.TrimEnd("-RP");
}
var index = title.LastIndexOf('-'); var index = title.LastIndexOf('-');
@ -275,6 +297,19 @@ namespace NzbDrone.Core.Parser
return group; return group;
} }
public static string RemoveFileExtension(string title)
{
if (!title.ContainsInvalidPathChars())
{
if (MediaFiles.MediaFileExtensions.Extensions.Contains(Path.GetExtension(title).ToLower()))
{
title = Path.Combine(Path.GetDirectoryName(title), Path.GetFileNameWithoutExtension(title));
}
}
return title;
}
private static SeriesTitleInfo GetSeriesTitleInfo(string title) private static SeriesTitleInfo GetSeriesTitleInfo(string title)
{ {
var seriesTitleInfo = new SeriesTitleInfo(); var seriesTitleInfo = new SeriesTitleInfo();
@ -511,6 +546,14 @@ namespace NzbDrone.Core.Parser
return false; return false;
} }
var titleWithoutExtension = RemoveFileExtension(title);
if (RejectHashedReleasesRegex.Any(v => v.IsMatch(titleWithoutExtension)))
{
Logger.Debug("Rejected Hashed Release Title: " + title);
return false;
}
return true; return true;
} }
} }

View File

@ -3,7 +3,6 @@ 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.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Core.DataAugmentation.Scene; using NzbDrone.Core.DataAugmentation.Scene;
using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.IndexerSearch.Definitions;

View File

@ -97,6 +97,8 @@ namespace NzbDrone.Core.Tv
public Series FindByTitle(string title) public Series FindByTitle(string title)
{ {
title = Parser.Parser.CleanSeriesTitle(title);
var tvdbId = _sceneMappingService.GetTvDbId(title); var tvdbId = _sceneMappingService.GetTvDbId(title);
if (tvdbId.HasValue) if (tvdbId.HasValue)
@ -104,7 +106,7 @@ namespace NzbDrone.Core.Tv
return FindByTvdbId(tvdbId.Value); return FindByTvdbId(tvdbId.Value);
} }
return _seriesRepository.FindByTitle(Parser.Parser.CleanSeriesTitle(title)); return _seriesRepository.FindByTitle(title);
} }
public Series FindByTitleInexact(string title) public Series FindByTitleInexact(string title)

View File

@ -26,5 +26,10 @@ namespace NzbDrone.Core.Validation
ruleBuilder.SetValidator(new NotEmptyValidator(null)); ruleBuilder.SetValidator(new NotEmptyValidator(null));
return ruleBuilder.SetValidator(new RegularExpressionValidator("^http(?:s)?://[a-z0-9-.]+", RegexOptions.IgnoreCase)).WithMessage("must be valid URL that"); return ruleBuilder.SetValidator(new RegularExpressionValidator("^http(?:s)?://[a-z0-9-.]+", RegexOptions.IgnoreCase)).WithMessage("must be valid URL that");
} }
public static IRuleBuilderOptions<T, int> ValidPort<T>(this IRuleBuilder<T, int> ruleBuilder)
{
return ruleBuilder.SetValidator(new InclusiveBetweenValidator(0, 65535));
}
} }
} }

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions;
using NLog; using NLog;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
@ -13,6 +14,7 @@ namespace NzbDrone.Host.AccessControl
public class SslAdapter : ISslAdapter public class SslAdapter : ISslAdapter
{ {
private const string APP_ID = "C2172AF4-F9A6-4D91-BAEE-C2E4EE680613"; private const string APP_ID = "C2172AF4-F9A6-4D91-BAEE-C2E4EE680613";
private static readonly Regex CertificateHashRegex = new Regex(@"^\s+(?:Certificate Hash\s+:\s+)(?<hash>\w+)", RegexOptions.Compiled);
private readonly INetshProvider _netshProvider; private readonly INetshProvider _netshProvider;
private readonly IConfigFileProvider _configFileProvider; private readonly IConfigFileProvider _configFileProvider;
@ -54,7 +56,32 @@ namespace NzbDrone.Host.AccessControl
if (output == null || !output.Standard.Any()) return false; if (output == null || !output.Standard.Any()) return false;
var hashLine = output.Standard.SingleOrDefault(line => CertificateHashRegex.IsMatch(line));
if (hashLine != null)
{
var match = CertificateHashRegex.Match(hashLine);
if (match.Success)
{
if (match.Groups["hash"].Value != _configFileProvider.SslCertHash)
{
Unregister();
return false;
}
}
}
return output.Standard.Any(line => line.Contains(ipPort)); return output.Standard.Any(line => line.Contains(ipPort));
} }
private void Unregister()
{
var ipPort = "0.0.0.0:" + _configFileProvider.SslPort;
var arguments = String.Format("http delete sslcert ipport={0}", ipPort);
_netshProvider.Run(arguments);
}
} }
} }

View File

@ -24,7 +24,6 @@ namespace NzbDrone.Host
private readonly PriorityMonitor _priorityMonitor; private readonly PriorityMonitor _priorityMonitor;
private readonly IStartupContext _startupContext; private readonly IStartupContext _startupContext;
private readonly IBrowserService _browserService; private readonly IBrowserService _browserService;
private readonly IProcessProvider _processProvider;
private readonly Logger _logger; private readonly Logger _logger;
public NzbDroneServiceFactory(IConfigFileProvider configFileProvider, public NzbDroneServiceFactory(IConfigFileProvider configFileProvider,
@ -33,7 +32,6 @@ namespace NzbDrone.Host
PriorityMonitor priorityMonitor, PriorityMonitor priorityMonitor,
IStartupContext startupContext, IStartupContext startupContext,
IBrowserService browserService, IBrowserService browserService,
IProcessProvider processProvider,
Logger logger) Logger logger)
{ {
_configFileProvider = configFileProvider; _configFileProvider = configFileProvider;
@ -42,7 +40,6 @@ namespace NzbDrone.Host
_priorityMonitor = priorityMonitor; _priorityMonitor = priorityMonitor;
_startupContext = startupContext; _startupContext = startupContext;
_browserService = browserService; _browserService = browserService;
_processProvider = processProvider;
_logger = logger; _logger = logger;
} }

View File

@ -34,6 +34,7 @@ namespace NzbDrone.Host
_container = MainAppContainerBuilder.BuildContainer(startupContext); _container = MainAppContainerBuilder.BuildContainer(startupContext);
_container.Resolve<IAppFolderFactory>().Register(); _container.Resolve<IAppFolderFactory>().Register();
_container.Resolve<IProvidePidFile>().Write();
var appMode = GetApplicationMode(startupContext); var appMode = GetApplicationMode(startupContext);

View File

@ -57,7 +57,13 @@ define(
this.$el.addClass(this.className); this.$el.addClass(this.className);
this.ui.seriesSearch.keypress(function () { this.ui.seriesSearch.keyup(function (e) {
//Ignore special keys: http://www.javascripter.net/faq/keycodes.htm
if (_.contains([9, 16, 17, 18, 19, 20, 33, 34, 35, 36, 37, 38, 39, 40, 91, 92, 93 ], e.keyCode)) {
return;
}
self.searchResult.close(); self.searchResult.close();
self._abortExistingSearch(); self._abortExistingSearch();
self.throttledSearch({ self.throttledSearch({

View File

@ -2,17 +2,19 @@
define( define(
[ [
'underscore',
'Cells/ToggleCell', 'Cells/ToggleCell',
'Series/SeriesCollection', 'Series/SeriesCollection',
'Shared/Messenger' 'Shared/Messenger'
], function (ToggleCell, SeriesCollection, Messenger) { ], function (_, ToggleCell, SeriesCollection, Messenger) {
return ToggleCell.extend({ return ToggleCell.extend({
className: 'toggle-cell episode-monitored', className: 'toggle-cell episode-monitored',
_originalOnClick: ToggleCell.prototype._onClick, _originalOnClick: ToggleCell.prototype._onClick,
_onClick: function () { _onClick: function (e) {
var series = SeriesCollection.get(this.model.get('seriesId')); var series = SeriesCollection.get(this.model.get('seriesId'));
if (!series.get('monitored')) { if (!series.get('monitored')) {
@ -25,7 +27,41 @@ define(
return; return;
} }
if (e.shiftKey) {
this._selectRange();
return;
}
this._originalOnClick.apply(this, arguments); this._originalOnClick.apply(this, arguments);
this.model.episodeCollection.lastToggled = this.model;
},
_selectRange: function () {
var episodeCollection = this.model.episodeCollection;
var lastToggled = episodeCollection.lastToggled;
if (!lastToggled) {
return;
}
var currentIndex = episodeCollection.indexOf(this.model);
var lastIndex = episodeCollection.indexOf(lastToggled);
var low = Math.min(currentIndex, lastIndex);
var high = Math.max(currentIndex, lastIndex);
var range = _.range(low + 1, high);
_.each(range, function (index) {
var model = episodeCollection.at(index);
model.set('monitored', lastToggled.get('monitored'));
model.save();
});
this.model.set('monitored', lastToggled.get('monitored'));
this.model.save();
this.model.episodeCollection.lastToggled = undefined;
} }
}); });
}); });

View File

@ -24,8 +24,8 @@ define(
this.$('i').addClass('icon-spinner icon-spin'); this.$('i').addClass('icon-spinner icon-spin');
this.model.save().always(function () { this.model.save().always(function () {
self.render(); self.render();
}); });
}, },
render: function () { render: function () {

View File

@ -2,6 +2,7 @@
@import "../Content/Bootstrap/variables"; @import "../Content/Bootstrap/variables";
@import "../Content/Bootstrap/buttons"; @import "../Content/Bootstrap/buttons";
@import "../Shared/Styles/clickable"; @import "../Shared/Styles/clickable";
@import "../Content/mixins";
.episode-title-cell { .episode-title-cell {
.btn-link; .btn-link;
@ -31,6 +32,7 @@
.toggle-cell{ .toggle-cell{
.clickable(); .clickable();
.not-selectable;
} }
.approval-status-cell { .approval-status-cell {

View File

@ -0,0 +1,11 @@
.selectable() {
-moz-user-select : all;
-webkit-user-select : all;
-ms-user-select : all;
}
.not-selectable() {
-moz-user-select : none;
-webkit-user-select : none;
-ms-user-select : none;
}

View File

@ -92,11 +92,19 @@ define(
initialize: function (options) { initialize: function (options) {
if (!options.episodeCollection) { if (!options.episodeCollection) {
throw 'episodeCollection is needed'; throw 'episodeCollection is needed';
} }
this.episodeCollection = options.episodeCollection.bySeason(this.model.get('seasonNumber')); this.episodeCollection = options.episodeCollection.bySeason(this.model.get('seasonNumber'));
var self = this;
this.episodeCollection.each(function (model) {
model.episodeCollection = self.episodeCollection;
});
this.series = options.series; this.series = options.series;
this.showingEpisodes = this._shouldShowEpisodes(); this.showingEpisodes = this._shouldShowEpisodes();
@ -249,6 +257,34 @@ define(
this.templateHelpers.showingEpisodes = this.showingEpisodes; this.templateHelpers.showingEpisodes = this.showingEpisodes;
this.render(); this.render();
},
_episodeMonitoredToggled: function (options) {
var model = options.model;
var shiftKey = options.shiftKey;
if (!this.episodeCollection.get(model.get('id'))) {
return;
}
if (!shiftKey) {
return;
}
var lastToggled = this.episodeCollection.lastToggled;
if (!lastToggled) {
return;
}
var currentIndex = this.episodeCollection.indexOf(model);
var lastIndex = this.episodeCollection.indexOf(lastToggled);
var low = Math.min(currentIndex, lastIndex);
var high = Math.max(currentIndex, lastIndex);
var range = _.range(low + 1, high);
this.episodeCollection.lastToggled = model;
} }
}); });
}); });

View File

@ -178,6 +178,7 @@
} }
.series-season { .series-season {
.episode-number-cell { .episode-number-cell {
width : 22px; width : 22px;
} }

View File

@ -55,6 +55,7 @@
</div> </div>
</div> </div>
{{#if_windows}}
<div class="control-group advanced-setting"> <div class="control-group advanced-setting">
<label class="control-label">SSL Cert Hash</label> <label class="control-label">SSL Cert Hash</label>
@ -62,6 +63,7 @@
<input type="text" name="sslCertHash"/> <input type="text" name="sslCertHash"/>
</div> </div>
</div> </div>
{{/if_windows}}
</div> </div>
<div class="control-group"> <div class="control-group">

View File

@ -27,6 +27,7 @@ define(
'click .x-delete' : '_deleteNotification', 'click .x-delete' : '_deleteNotification',
'click .x-back' : '_back', 'click .x-back' : '_back',
'click .x-test' : '_test', 'click .x-test' : '_test',
'click .x-cancel' : '_cancel',
'change .x-on-download': '_onDownloadChanged' 'change .x-on-download': '_onDownloadChanged'
}, },
@ -63,12 +64,23 @@ define(
} }
}, },
_cancel: function () {
if (this.model.isNew()) {
this.model.destroy();
vent.trigger(vent.Commands.CloseModalCommand);
}
},
_deleteNotification: function () { _deleteNotification: function () {
var view = new DeleteView({ model: this.model }); var view = new DeleteView({ model: this.model });
AppLayout.modalRegion.show(view); AppLayout.modalRegion.show(view);
}, },
_back: function () { _back: function () {
if (this.model.isNew()) {
this.model.destroy();
}
require('Settings/Notifications/SchemaModal').open(this.notificationCollection); require('Settings/Notifications/SchemaModal').open(this.notificationCollection);
}, },

View File

@ -87,7 +87,7 @@
{{/if}} {{/if}}
<button class="btn x-test">test <i class="x-test-icon icon-nd-test"/></button> <button class="btn x-test">test <i class="x-test-icon icon-nd-test"/></button>
<button class="btn" data-dismiss="modal">cancel</button> <button class="btn x-cancel">cancel</button>
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-primary x-save">save</button> <button class="btn btn-primary x-save">save</button>

View File

@ -11,8 +11,8 @@
<label class="checkbox toggle well"> <label class="checkbox toggle well">
<input type="checkbox" class="x-advanced-settings"/> <input type="checkbox" class="x-advanced-settings"/>
<p> <p>
<span>Show</span> <span>Shown</span>
<span>Hide</span> <span>Hidden</span>
</p> </p>
<div class="btn btn-warning slide-button"/> <div class="btn btn-warning slide-button"/>
</label> </label>

View File

@ -8,11 +8,11 @@ define(
var vent = new Backbone.Wreqr.EventAggregator(); var vent = new Backbone.Wreqr.EventAggregator();
vent.Events = { vent.Events = {
SeriesAdded : 'series:added', SeriesAdded : 'series:added',
SeriesDeleted : 'series:deleted', SeriesDeleted : 'series:deleted',
CommandComplete : 'command:complete', CommandComplete : 'command:complete',
ServerUpdated : 'server:updated', ServerUpdated : 'server:updated',
EpisodeFileDeleted: 'episodefile:deleted' EpisodeFileDeleted : 'episodefile:deleted'
}; };
vent.Commands = { vent.Commands = {