diff --git a/src/NzbDrone.Api/Authentication/EnableStatelessAuthInNancy.cs b/src/NzbDrone.Api/Authentication/EnableStatelessAuthInNancy.cs index 1bcb8685e..e6aaeae27 100644 --- a/src/NzbDrone.Api/Authentication/EnableStatelessAuthInNancy.cs +++ b/src/NzbDrone.Api/Authentication/EnableStatelessAuthInNancy.cs @@ -5,7 +5,6 @@ using Nancy.Bootstrapper; using NzbDrone.Api.Extensions; using NzbDrone.Api.Extensions.Pipelines; using NzbDrone.Common; -using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Configuration; namespace NzbDrone.Api.Authentication @@ -28,11 +27,9 @@ namespace NzbDrone.Api.Authentication { Response response = null; - var authorizationHeader = context.Request.Headers.Authorization; - var apiKeyHeader = context.Request.Headers["X-Api-Key"].FirstOrDefault(); - var apiKey = apiKeyHeader.IsNullOrWhiteSpace() ? authorizationHeader : apiKeyHeader; + var apiKey = GetApiKey(context); - if (context.Request.IsApiRequest() && !ValidApiKey(apiKey)) + if ((context.Request.IsApiRequest() || context.Request.IsFeedRequest()) && !ValidApiKey(apiKey)) { response = new Response { StatusCode = HttpStatusCode.Unauthorized }; } @@ -46,5 +43,23 @@ namespace NzbDrone.Api.Authentication 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; + } } } \ No newline at end of file diff --git a/src/NzbDrone.Api/Config/HostConfigModule.cs b/src/NzbDrone.Api/Config/HostConfigModule.cs index 6d818db6a..26c184c45 100644 --- a/src/NzbDrone.Api/Config/HostConfigModule.cs +++ b/src/NzbDrone.Api/Config/HostConfigModule.cs @@ -1,7 +1,9 @@ using System.Linq; using System.Reflection; using FluentValidation; +using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Validation; using Omu.ValueInjecter; 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.Password).NotEmpty().When(c => c.AuthenticationEnabled); - SharedValidator.RuleFor(c => c.SslPort).InclusiveBetween(1, 65535).When(c => c.EnableSsl); - SharedValidator.RuleFor(c => c.SslCertHash).NotEmpty().When(c => c.EnableSsl); + SharedValidator.RuleFor(c => c.SslPort).ValidPort().When(c => c.EnableSsl); + SharedValidator.RuleFor(c => c.SslCertHash).NotEmpty().When(c => c.EnableSsl && OsInfo.IsWindows); } private HostConfigResource GetHostConfig() diff --git a/src/NzbDrone.Api/Extensions/RequestExtensions.cs b/src/NzbDrone.Api/Extensions/RequestExtensions.cs index 02686deb6..a07ab687d 100644 --- a/src/NzbDrone.Api/Extensions/RequestExtensions.cs +++ b/src/NzbDrone.Api/Extensions/RequestExtensions.cs @@ -10,6 +10,11 @@ namespace NzbDrone.Api.Extensions 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) { return request.Path.StartsWith("/signalr/", StringComparison.InvariantCultureIgnoreCase); diff --git a/src/NzbDrone.Api/History/HistoryResource.cs b/src/NzbDrone.Api/History/HistoryResource.cs index e95330c52..2d9d0a07d 100644 --- a/src/NzbDrone.Api/History/HistoryResource.cs +++ b/src/NzbDrone.Api/History/HistoryResource.cs @@ -5,7 +5,7 @@ using NzbDrone.Api.REST; using NzbDrone.Api.Series; using NzbDrone.Core.History; using NzbDrone.Core.Qualities; -using NzbDrone.Core.Tv; + namespace NzbDrone.Api.History { diff --git a/src/NzbDrone.Api/Validation/RuleBuilderExtensions.cs b/src/NzbDrone.Api/Validation/RuleBuilderExtensions.cs index 42f0d8db0..e88607b65 100644 --- a/src/NzbDrone.Api/Validation/RuleBuilderExtensions.cs +++ b/src/NzbDrone.Api/Validation/RuleBuilderExtensions.cs @@ -1,7 +1,6 @@ using System.Text.RegularExpressions; using FluentValidation; using FluentValidation.Validators; -using NzbDrone.Core.Validation.Paths; namespace NzbDrone.Api.Validation { diff --git a/src/NzbDrone.Common/EnvironmentInfo/AppFolderFactory.cs b/src/NzbDrone.Common/EnvironmentInfo/AppFolderFactory.cs index d45f4269f..3f470a963 100644 --- a/src/NzbDrone.Common/EnvironmentInfo/AppFolderFactory.cs +++ b/src/NzbDrone.Common/EnvironmentInfo/AppFolderFactory.cs @@ -1,9 +1,6 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Security.AccessControl; using System.Security.Principal; -using System.Text; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.Instrumentation; diff --git a/src/NzbDrone.Common/NzbDrone.Common.csproj b/src/NzbDrone.Common/NzbDrone.Common.csproj index 086bd8d80..b2dd6301f 100644 --- a/src/NzbDrone.Common/NzbDrone.Common.csproj +++ b/src/NzbDrone.Common/NzbDrone.Common.csproj @@ -103,6 +103,7 @@ + diff --git a/src/NzbDrone.Common/Processes/PidFileProvider.cs b/src/NzbDrone.Common/Processes/PidFileProvider.cs new file mode 100644 index 000000000..c72832a68 --- /dev/null +++ b/src/NzbDrone.Common/Processes/PidFileProvider.cs @@ -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; + } + } + } + } +} diff --git a/src/NzbDrone.Common/StringExtensions.cs b/src/NzbDrone.Common/StringExtensions.cs index 77ad367cb..21b1db970 100644 --- a/src/NzbDrone.Common/StringExtensions.cs +++ b/src/NzbDrone.Common/StringExtensions.cs @@ -48,6 +48,14 @@ namespace NzbDrone.Common 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) { return CollapseSpace.Replace(text, " ").Trim(); diff --git a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupDuplicateMetadataFilesFixture.cs b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupDuplicateMetadataFilesFixture.cs index 839acf23b..adb742dfa 100644 --- a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupDuplicateMetadataFilesFixture.cs +++ b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupDuplicateMetadataFilesFixture.cs @@ -66,5 +66,113 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers Subject.Clean(); 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.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.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.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.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.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.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.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.CreateNew() + .BuildNew(); + + Db.Insert(file); + Subject.Clean(); + AllStoredModels.Count.Should().Be(1); + } } } diff --git a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedMetadataFilesFixture.cs b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedMetadataFilesFixture.cs index 0e6bb9fc6..4ea0046c3 100644 --- a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedMetadataFilesFixture.cs +++ b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedMetadataFilesFixture.cs @@ -3,6 +3,7 @@ using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Housekeeping.Housekeepers; using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Metadata; using NzbDrone.Core.Metadata.Files; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; @@ -81,5 +82,43 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers Subject.Clean(); AllStoredModels.Should().HaveCount(1); } + + [Test] + public void should_delete_episode_metadata_files_that_have_episodefileid_of_zero() + { + var series = Builder.CreateNew() + .BuildNew(); + + Db.Insert(series); + + var metadataFile = Builder.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.CreateNew() + .BuildNew(); + + Db.Insert(series); + + var metadataFile = Builder.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); + } } } diff --git a/src/NzbDrone.Core.Test/OrganizerTests/GetNewFilenameFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/GetNewFilenameFixture.cs index 67afd0a44..6e1258888 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/GetNewFilenameFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/GetNewFilenameFixture.cs @@ -405,5 +405,35 @@ namespace NzbDrone.Core.Test.OrganizerTests Subject.BuildFilename(new List { episode }, new Series { Title = "30 Rock" }, _episodeFile) .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.CreateNew() + .With(e => e.Title = "Part 1") + .With(e => e.SeasonNumber = 6) + .With(e => e.EpisodeNumber = 6) + .Build(); + + Subject.BuildFilename(new List { 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.CreateNew() + .With(e => e.Title = "Part 1") + .With(e => e.SeasonNumber = 6) + .With(e => e.EpisodeNumber = 6) + .Build(); + + Subject.BuildFilename(new List { episode }, new Series { Title = "Chicago P.D.." }, _episodeFile) + .Should().Be("Chicago.P.D.S06E06.Part.1"); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/ParserTests/CrapParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/CrapParserFixture.cs index a304518f1..83c9315e7 100644 --- a/src/NzbDrone.Core.Test/ParserTests/CrapParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/CrapParserFixture.cs @@ -7,6 +7,7 @@ using NzbDrone.Core.Parser; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; using NzbDrone.Test.Common; +using System.Text; namespace NzbDrone.Core.Test.ParserTests { @@ -29,10 +30,60 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("THIS SHOULD NEVER PARSE")] [TestCase("Vh1FvU3bJXw6zs8EEUX4bMo5vbbMdHghxHirc.mkv")] [TestCase("0e895c37245186812cb08aab1529cf8ee389dd05.mkv")] + [TestCase("08bbc153931ce3ca5fcafe1b92d3297285feb061.mkv")] + [TestCase("185d86a343e39f3341e35c4dad3ff159")] public void should_not_parse_crap(string title) { Parser.Parser.ParseTitle(title).Should().BeNull(); 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); + } } } diff --git a/src/NzbDrone.Core.Test/ParserTests/DailyEpisodeParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/DailyEpisodeParserFixture.cs index e889dc607..66b9b5733 100644 --- a/src/NzbDrone.Core.Test/ParserTests/DailyEpisodeParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/DailyEpisodeParserFixture.cs @@ -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.13.02.PDTV.XviD-C4TV", "2020nz", 2012, 2, 13)] [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) { var result = Parser.Parser.ParseTitle(postTitle); diff --git a/src/NzbDrone.Core.Test/ParserTests/HashedReleaseFixture.cs b/src/NzbDrone.Core.Test/ParserTests/HashedReleaseFixture.cs index ea17fda0d..c763285ae 100644 --- a/src/NzbDrone.Core.Test/ParserTests/HashedReleaseFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/HashedReleaseFixture.cs @@ -12,17 +12,31 @@ namespace NzbDrone.Core.Test.ParserTests { 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", "WEBDL-720p", "Mercury" }, 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", "WEBDL-720p", "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" } }; diff --git a/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs index cc3c3e7ec..7e84877d4 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs @@ -24,11 +24,17 @@ namespace NzbDrone.Core.Test.ParserTests } [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"; 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); + } } } diff --git a/src/NzbDrone.Core/Datastore/Migration/049_fix_dognzb_url.cs b/src/NzbDrone.Core/Datastore/Migration/049_fix_dognzb_url.cs new file mode 100644 index 000000000..ebbe8d8c0 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/049_fix_dognzb_url.cs @@ -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%'"); + } + } +} diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupDuplicateMetadataFiles.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupDuplicateMetadataFiles.cs index 3c7ba56b2..1b720b9e9 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupDuplicateMetadataFiles.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupDuplicateMetadataFiles.cs @@ -19,6 +19,8 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers _logger.Debug("Running cleanup of duplicate metadata files"); DeleteDuplicateSeriesMetadata(); + DeleteDuplicateEpisodeMetadata(); + DeleteDuplicateEpisodeImages(); } private void DeleteDuplicateSeriesMetadata() @@ -33,5 +35,31 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers 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 + )"); + } } } diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedMetadataFiles.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedMetadataFiles.cs index 88ee87ad3..e889b3214 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedMetadataFiles.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedMetadataFiles.cs @@ -20,6 +20,7 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers DeleteOrphanedBySeries(); DeleteOrphanedByEpisodeFile(); + DeleteWhereEpisodeFileIsZero(); } private void DeleteOrphanedBySeries() @@ -46,5 +47,16 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers WHERE MetadataFiles.EpisodeFileId > 0 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)"); + } } } diff --git a/src/NzbDrone.Core/IndexerSearch/EpisodeSearchService.cs b/src/NzbDrone.Core/IndexerSearch/EpisodeSearchService.cs index 049677f78..4bca174db 100644 --- a/src/NzbDrone.Core/IndexerSearch/EpisodeSearchService.cs +++ b/src/NzbDrone.Core/IndexerSearch/EpisodeSearchService.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using NLog; using NzbDrone.Common; @@ -13,7 +14,8 @@ namespace NzbDrone.Core.IndexerSearch { public interface IEpisodeSearchService { - void MissingEpisodesAiredAfter(DateTime dateTime); + void MissingEpisodesAiredAfter(DateTime dateTime, IEnumerable grabbed + ); } public class MissingEpisodeSearchService : IEpisodeSearchService, IExecute, IExecute @@ -37,11 +39,12 @@ namespace NzbDrone.Core.IndexerSearch _logger = logger; } - public void MissingEpisodesAiredAfter(DateTime dateTime) + public void MissingEpisodesAiredAfter(DateTime dateTime, IEnumerable grabbed) { var missing = _episodeService.EpisodesBetweenDates(dateTime, DateTime.UtcNow) .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(); var downloadedCount = 0; diff --git a/src/NzbDrone.Core/Indexers/Newznab/Newznab.cs b/src/NzbDrone.Core/Indexers/Newznab/Newznab.cs index cb89009b9..c4c8288a8 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/Newznab.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/Newznab.cs @@ -43,7 +43,7 @@ namespace NzbDrone.Core.Indexers.Newznab Enable = false, Name = "Dognzb.cr", Implementation = GetType().Name, - Settings = GetSettings("https://dognzb.cr", new List()) + Settings = GetSettings("https://api.dognzb.cr", new List()) }); list.Add(new IndexerDefinition diff --git a/src/NzbDrone.Core/Indexers/RssSyncService.cs b/src/NzbDrone.Core/Indexers/RssSyncService.cs index 07b26bfff..540b4e52e 100644 --- a/src/NzbDrone.Core/Indexers/RssSyncService.cs +++ b/src/NzbDrone.Core/Indexers/RssSyncService.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Generic; using System.Linq; using NLog; using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Download; using NzbDrone.Core.IndexerSearch; using NzbDrone.Core.Instrumentation.Extensions; @@ -11,7 +13,7 @@ namespace NzbDrone.Core.Indexers { public interface IRssSyncService { - void Sync(); + List Sync(); } public class RssSyncService : IRssSyncService, IExecute @@ -36,7 +38,7 @@ namespace NzbDrone.Core.Indexers } - public void Sync() + public List Sync() { _logger.ProgressInfo("Starting RSS Sync"); @@ -45,16 +47,18 @@ namespace NzbDrone.Core.Indexers var downloaded = _downloadApprovedReports.DownloadApproved(decisions); _logger.ProgressInfo("RSS Sync Completed. Reports found: {0}, Reports downloaded: {1}", reports.Count, downloaded.Count()); + + return downloaded; } public void Execute(RssSyncCommand message) { - Sync(); + var downloaded = Sync(); 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); - _episodeSearchService.MissingEpisodesAiredAfter(message.LastExecutionTime.Value.AddDays(-1)); + _episodeSearchService.MissingEpisodesAiredAfter(message.LastExecutionTime.Value.AddDays(-1), downloaded.SelectMany(d => d.RemoteEpisode.Episodes).Select(e => e.Id)); } } } diff --git a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs index 6e86e1077..4e2801c95 100644 --- a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs +++ b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs @@ -1,4 +1,6 @@ -using System.Diagnostics; +using System; +using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using NLog; @@ -76,7 +78,7 @@ namespace NzbDrone.Core.MediaFiles } 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(); _logger.Trace("Finished getting episode files for: {0} [{1}]", series, videoFilesStopwatch.Elapsed); diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs index 34eac41c4..5e3086835 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs @@ -7,6 +7,7 @@ using NzbDrone.Common; using NzbDrone.Common.Disk; using NzbDrone.Common.EnsureThat; using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Exceptions; using NzbDrone.Core.Configuration; using NzbDrone.Core.Organizer; using NzbDrone.Core.Parser.Model; @@ -172,15 +173,9 @@ namespace NzbDrone.Core.MediaFiles catch (Exception ex) { - if (ex is UnauthorizedAccessException || ex is InvalidOperationException) - { - _logger.Debug("Unable to apply permissions to: ", path); - _logger.DebugException(ex.Message, ex); - } - else - { - throw; - } + + _logger.WarnException("Unable to apply permissions to: " + path, ex); + _logger.DebugException(ex.Message, ex); } } diff --git a/src/NzbDrone.Core/MetaData/Consumers/Xbmc/XbmcMetadata.cs b/src/NzbDrone.Core/MetaData/Consumers/Xbmc/XbmcMetadata.cs index 78a61588b..4089c6863 100644 --- a/src/NzbDrone.Core/MetaData/Consumers/Xbmc/XbmcMetadata.cs +++ b/src/NzbDrone.Core/MetaData/Consumers/Xbmc/XbmcMetadata.cs @@ -247,6 +247,8 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc xws.OmitXmlDeclaration = true; 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)) { 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("rating", (decimal)series.Ratings.Percentage/10)); tvShow.Add(new XElement("plot", series.Overview)); - - //Todo: probably will need to use TVDB to use this feature... -// tvShow.Add(new XElement("episodeguide", new XElement("url", episodeGuideUrl))); -// 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("id", series.TvdbId)); diff --git a/src/NzbDrone.Core/Metadata/ExistingMetadataService.cs b/src/NzbDrone.Core/Metadata/ExistingMetadataService.cs index cf625706f..0a4c68cdf 100644 --- a/src/NzbDrone.Core/Metadata/ExistingMetadataService.cs +++ b/src/NzbDrone.Core/Metadata/ExistingMetadataService.cs @@ -40,7 +40,10 @@ namespace NzbDrone.Core.Metadata _logger.Debug("Looking for existing metadata in {0}", message.Series.Path); 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 metadataFiles = new List(); diff --git a/src/NzbDrone.Core/MetadataSource/TraktProxy.cs b/src/NzbDrone.Core/MetadataSource/TraktProxy.cs index 2743ab083..664778b63 100644 --- a/src/NzbDrone.Core/MetadataSource/TraktProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/TraktProxy.cs @@ -20,7 +20,7 @@ namespace NzbDrone.Core.MetadataSource { private readonly Logger _logger; 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) { @@ -168,7 +168,9 @@ namespace NzbDrone.Core.MetadataSource phrase = phrase.RemoveAccent().ToLower(); phrase = InvalidSearchCharRegex.Replace(phrase, ""); phrase = CollapseSpaceRegex.Replace(phrase, " ").Trim().ToLower(); + phrase = phrase.Trim('-'); phrase = HttpUtility.UrlEncode(phrase); + return phrase; } diff --git a/src/NzbDrone.Core/Notifications/Xbmc/XbmcService.cs b/src/NzbDrone.Core/Notifications/Xbmc/XbmcService.cs index 3a1ac4249..2845a5f6f 100644 --- a/src/NzbDrone.Core/Notifications/Xbmc/XbmcService.cs +++ b/src/NzbDrone.Core/Notifications/Xbmc/XbmcService.cs @@ -61,7 +61,7 @@ namespace NzbDrone.Core.Notifications.Xbmc 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>(response); var versionObject = result.Result.Property("version"); diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index c09bc0c6d..142ec8bb4 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -46,6 +46,8 @@ namespace NzbDrone.Core.Organizer public static readonly Regex SeriesTitleRegex = new Regex(@"(?\{(?:Series)(?\s|\.|-|_)Title\})", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex FilenameCleanupRegex = new Regex(@"\.{2,}", RegexOptions.Compiled); + private static readonly char[] EpisodeTitleTrimCharaters = new[] { ' ', '.', '?' }; public FileNameBuilder(INamingConfigService namingConfigService, @@ -90,6 +92,7 @@ namespace NzbDrone.Core.Organizer var sortedEpisodes = episodes.OrderBy(e => e.EpisodeNumber).ToList(); var pattern = namingConfig.StandardEpisodeFormat; + var episodeTitles = new List { sortedEpisodes.First().Title.TrimEnd(EpisodeTitleTrimCharaters) @@ -153,8 +156,11 @@ namespace NzbDrone.Core.Organizer tokenValues.Add("{Episode Title}", GetEpisodeTitle(episodeTitles)); 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) diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 89d1257d5..5b4cf8d2d 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -43,7 +43,7 @@ namespace NzbDrone.Core.Parser RegexOptions.IgnoreCase | RegexOptions.Compiled), //Episodes without a title, Single (S01E05, 1x05) AND Multi (S01E04E05, 1x04x05, etc) - new Regex(@"^(?:S?(?(?\d{2,3}(?!\d+)))+(?![\da-z]))", + new Regex(@"^(?:S?(?(?\d{2,3}(?!\d+)))+)", RegexOptions.IgnoreCase | RegexOptions.Compiled), //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), //Anime - Title Absolute Episode Number [SubGroup] - new Regex(@"^(?.+?)(?:(?:_|-|\s|\.)+(?<absoluteepisode>\d{2,3}))+(?:.+?)\[(?<subgroup>.+?)\](?:\.|$)", + new Regex(@"^(?<title>.+?)(?:(?:_|-|\s|\.)+(?<absoluteepisode>\d{3}(?!\d+)))+(?:.+?)\[(?<subgroup>.+?)\](?:\.|$)", RegexOptions.IgnoreCase | RegexOptions.Compiled), //Supports 103/113 naming @@ -92,7 +92,7 @@ namespace NzbDrone.Core.Parser RegexOptions.IgnoreCase | RegexOptions.Compiled), //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), //Anime - Title Absolute Episode Number @@ -100,6 +100,18 @@ namespace NzbDrone.Core.Parser 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|_", RegexOptions.IgnoreCase | RegexOptions.Compiled); @@ -155,6 +167,17 @@ namespace NzbDrone.Core.Parser if (!ValidateBeforeParsing(title)) return null; 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); foreach (var regex in ReportTitleRegex) @@ -245,10 +268,9 @@ namespace NzbDrone.Core.Parser title = title.Trim(); - if (!title.ContainsInvalidPathChars() && MediaFiles.MediaFileExtensions.Extensions.Contains(Path.GetExtension(title).ToLower())) - { - title = Path.GetFileNameWithoutExtension(title).Trim(); - } + title = RemoveFileExtension(title); + + title = title.TrimEnd("-RP"); var index = title.LastIndexOf('-'); @@ -275,6 +297,19 @@ namespace NzbDrone.Core.Parser 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) { var seriesTitleInfo = new SeriesTitleInfo(); @@ -511,6 +546,14 @@ namespace NzbDrone.Core.Parser return false; } + var titleWithoutExtension = RemoveFileExtension(title); + + if (RejectHashedReleasesRegex.Any(v => v.IsMatch(titleWithoutExtension))) + { + Logger.Debug("Rejected Hashed Release Title: " + title); + return false; + } + return true; } } diff --git a/src/NzbDrone.Core/Parser/ParsingService.cs b/src/NzbDrone.Core/Parser/ParsingService.cs index 35f73caf8..ce507912e 100644 --- a/src/NzbDrone.Core/Parser/ParsingService.cs +++ b/src/NzbDrone.Core/Parser/ParsingService.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using NLog; -using NzbDrone.Common; using NzbDrone.Common.Disk; using NzbDrone.Core.DataAugmentation.Scene; using NzbDrone.Core.IndexerSearch.Definitions; diff --git a/src/NzbDrone.Core/Tv/SeriesService.cs b/src/NzbDrone.Core/Tv/SeriesService.cs index ed51da640..9de113351 100644 --- a/src/NzbDrone.Core/Tv/SeriesService.cs +++ b/src/NzbDrone.Core/Tv/SeriesService.cs @@ -97,6 +97,8 @@ namespace NzbDrone.Core.Tv public Series FindByTitle(string title) { + title = Parser.Parser.CleanSeriesTitle(title); + var tvdbId = _sceneMappingService.GetTvDbId(title); if (tvdbId.HasValue) @@ -104,7 +106,7 @@ namespace NzbDrone.Core.Tv return FindByTvdbId(tvdbId.Value); } - return _seriesRepository.FindByTitle(Parser.Parser.CleanSeriesTitle(title)); + return _seriesRepository.FindByTitle(title); } public Series FindByTitleInexact(string title) diff --git a/src/NzbDrone.Core/Validation/RuleBuilderExtensions.cs b/src/NzbDrone.Core/Validation/RuleBuilderExtensions.cs index da8609e84..9bcc7e18a 100644 --- a/src/NzbDrone.Core/Validation/RuleBuilderExtensions.cs +++ b/src/NzbDrone.Core/Validation/RuleBuilderExtensions.cs @@ -26,5 +26,10 @@ namespace NzbDrone.Core.Validation ruleBuilder.SetValidator(new NotEmptyValidator(null)); 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)); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Host/AccessControl/SslAdapter.cs b/src/NzbDrone.Host/AccessControl/SslAdapter.cs index ddb2e7201..56319a318 100644 --- a/src/NzbDrone.Host/AccessControl/SslAdapter.cs +++ b/src/NzbDrone.Host/AccessControl/SslAdapter.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Text.RegularExpressions; using NLog; using NzbDrone.Core.Configuration; @@ -13,6 +14,7 @@ namespace NzbDrone.Host.AccessControl public class SslAdapter : ISslAdapter { 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 IConfigFileProvider _configFileProvider; @@ -54,7 +56,32 @@ namespace NzbDrone.Host.AccessControl 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)); } + + private void Unregister() + { + var ipPort = "0.0.0.0:" + _configFileProvider.SslPort; + var arguments = String.Format("http delete sslcert ipport={0}", ipPort); + + _netshProvider.Run(arguments); + } } } diff --git a/src/NzbDrone.Host/ApplicationServer.cs b/src/NzbDrone.Host/ApplicationServer.cs index c299c4870..c76fcc75c 100644 --- a/src/NzbDrone.Host/ApplicationServer.cs +++ b/src/NzbDrone.Host/ApplicationServer.cs @@ -24,7 +24,6 @@ namespace NzbDrone.Host private readonly PriorityMonitor _priorityMonitor; private readonly IStartupContext _startupContext; private readonly IBrowserService _browserService; - private readonly IProcessProvider _processProvider; private readonly Logger _logger; public NzbDroneServiceFactory(IConfigFileProvider configFileProvider, @@ -33,7 +32,6 @@ namespace NzbDrone.Host PriorityMonitor priorityMonitor, IStartupContext startupContext, IBrowserService browserService, - IProcessProvider processProvider, Logger logger) { _configFileProvider = configFileProvider; @@ -42,7 +40,6 @@ namespace NzbDrone.Host _priorityMonitor = priorityMonitor; _startupContext = startupContext; _browserService = browserService; - _processProvider = processProvider; _logger = logger; } diff --git a/src/NzbDrone.Host/Bootstrap.cs b/src/NzbDrone.Host/Bootstrap.cs index ca277851c..66bbe0cf3 100644 --- a/src/NzbDrone.Host/Bootstrap.cs +++ b/src/NzbDrone.Host/Bootstrap.cs @@ -34,6 +34,7 @@ namespace NzbDrone.Host _container = MainAppContainerBuilder.BuildContainer(startupContext); _container.Resolve<IAppFolderFactory>().Register(); + _container.Resolve<IProvidePidFile>().Write(); var appMode = GetApplicationMode(startupContext); diff --git a/src/UI/AddSeries/AddSeriesView.js b/src/UI/AddSeries/AddSeriesView.js index 7ac62beb4..199c823de 100644 --- a/src/UI/AddSeries/AddSeriesView.js +++ b/src/UI/AddSeries/AddSeriesView.js @@ -57,7 +57,13 @@ define( 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._abortExistingSearch(); self.throttledSearch({ diff --git a/src/UI/Cells/EpisodeMonitoredCell.js b/src/UI/Cells/EpisodeMonitoredCell.js index 72a3e2fee..5f6c7f689 100644 --- a/src/UI/Cells/EpisodeMonitoredCell.js +++ b/src/UI/Cells/EpisodeMonitoredCell.js @@ -2,17 +2,19 @@ define( [ + 'underscore', 'Cells/ToggleCell', 'Series/SeriesCollection', 'Shared/Messenger' - ], function (ToggleCell, SeriesCollection, Messenger) { + ], function (_, ToggleCell, SeriesCollection, Messenger) { return ToggleCell.extend({ className: 'toggle-cell episode-monitored', _originalOnClick: ToggleCell.prototype._onClick, - _onClick: function () { + _onClick: function (e) { + var series = SeriesCollection.get(this.model.get('seriesId')); if (!series.get('monitored')) { @@ -25,7 +27,41 @@ define( return; } + if (e.shiftKey) { + this._selectRange(); + + return; + } + 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; } }); }); diff --git a/src/UI/Cells/ToggleCell.js b/src/UI/Cells/ToggleCell.js index 8c6a17dcb..f2e215750 100644 --- a/src/UI/Cells/ToggleCell.js +++ b/src/UI/Cells/ToggleCell.js @@ -24,8 +24,8 @@ define( this.$('i').addClass('icon-spinner icon-spin'); this.model.save().always(function () { - self.render(); - }); + self.render(); + }); }, render: function () { diff --git a/src/UI/Cells/cells.less b/src/UI/Cells/cells.less index 8869f0779..712b66bb1 100644 --- a/src/UI/Cells/cells.less +++ b/src/UI/Cells/cells.less @@ -2,6 +2,7 @@ @import "../Content/Bootstrap/variables"; @import "../Content/Bootstrap/buttons"; @import "../Shared/Styles/clickable"; +@import "../Content/mixins"; .episode-title-cell { .btn-link; @@ -31,6 +32,7 @@ .toggle-cell{ .clickable(); + .not-selectable; } .approval-status-cell { diff --git a/src/UI/Content/mixins.less b/src/UI/Content/mixins.less new file mode 100644 index 000000000..999cc22d2 --- /dev/null +++ b/src/UI/Content/mixins.less @@ -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; +} \ No newline at end of file diff --git a/src/UI/Series/Details/SeasonLayout.js b/src/UI/Series/Details/SeasonLayout.js index 5f9620c75..6a0ee80d4 100644 --- a/src/UI/Series/Details/SeasonLayout.js +++ b/src/UI/Series/Details/SeasonLayout.js @@ -92,11 +92,19 @@ define( initialize: function (options) { + if (!options.episodeCollection) { throw 'episodeCollection is needed'; } 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.showingEpisodes = this._shouldShowEpisodes(); @@ -249,6 +257,34 @@ define( this.templateHelpers.showingEpisodes = this.showingEpisodes; 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; } }); }); diff --git a/src/UI/Series/series.less b/src/UI/Series/series.less index 96f0e1137..60cceb63f 100644 --- a/src/UI/Series/series.less +++ b/src/UI/Series/series.less @@ -178,6 +178,7 @@ } .series-season { + .episode-number-cell { width : 22px; } diff --git a/src/UI/Settings/General/GeneralViewTemplate.html b/src/UI/Settings/General/GeneralViewTemplate.html index c05f5f6e8..f26bfb6fe 100644 --- a/src/UI/Settings/General/GeneralViewTemplate.html +++ b/src/UI/Settings/General/GeneralViewTemplate.html @@ -55,6 +55,7 @@ </div> </div> + {{#if_windows}} <div class="control-group advanced-setting"> <label class="control-label">SSL Cert Hash</label> @@ -62,6 +63,7 @@ <input type="text" name="sslCertHash"/> </div> </div> + {{/if_windows}} </div> <div class="control-group"> diff --git a/src/UI/Settings/Notifications/NotificationEditView.js b/src/UI/Settings/Notifications/NotificationEditView.js index 161ebde96..f36caa448 100644 --- a/src/UI/Settings/Notifications/NotificationEditView.js +++ b/src/UI/Settings/Notifications/NotificationEditView.js @@ -27,6 +27,7 @@ define( 'click .x-delete' : '_deleteNotification', 'click .x-back' : '_back', 'click .x-test' : '_test', + 'click .x-cancel' : '_cancel', '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 () { var view = new DeleteView({ model: this.model }); AppLayout.modalRegion.show(view); }, _back: function () { + if (this.model.isNew()) { + this.model.destroy(); + } + require('Settings/Notifications/SchemaModal').open(this.notificationCollection); }, diff --git a/src/UI/Settings/Notifications/NotificationEditViewTemplate.html b/src/UI/Settings/Notifications/NotificationEditViewTemplate.html index e6abfa829..1676b8755 100644 --- a/src/UI/Settings/Notifications/NotificationEditViewTemplate.html +++ b/src/UI/Settings/Notifications/NotificationEditViewTemplate.html @@ -87,7 +87,7 @@ {{/if}} <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"> <button class="btn btn-primary x-save">save</button> diff --git a/src/UI/Settings/SettingsLayoutTemplate.html b/src/UI/Settings/SettingsLayoutTemplate.html index 62ee238fb..f5717ede2 100644 --- a/src/UI/Settings/SettingsLayoutTemplate.html +++ b/src/UI/Settings/SettingsLayoutTemplate.html @@ -11,8 +11,8 @@ <label class="checkbox toggle well"> <input type="checkbox" class="x-advanced-settings"/> <p> - <span>Show</span> - <span>Hide</span> + <span>Shown</span> + <span>Hidden</span> </p> <div class="btn btn-warning slide-button"/> </label> diff --git a/src/UI/vent.js b/src/UI/vent.js index 121edb83f..c50fc358d 100644 --- a/src/UI/vent.js +++ b/src/UI/vent.js @@ -8,11 +8,11 @@ define( var vent = new Backbone.Wreqr.EventAggregator(); vent.Events = { - SeriesAdded : 'series:added', - SeriesDeleted : 'series:deleted', - CommandComplete : 'command:complete', - ServerUpdated : 'server:updated', - EpisodeFileDeleted: 'episodefile:deleted' + SeriesAdded : 'series:added', + SeriesDeleted : 'series:deleted', + CommandComplete : 'command:complete', + ServerUpdated : 'server:updated', + EpisodeFileDeleted : 'episodefile:deleted' }; vent.Commands = {