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

View File

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

View File

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

View File

@ -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
{

View File

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

View File

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

View File

@ -103,6 +103,7 @@
<Compile Include="Messaging\IMessage.cs" />
<Compile Include="PathEqualityComparer.cs" />
<Compile Include="Processes\INzbDroneProcessProvider.cs" />
<Compile Include="Processes\PidFileProvider.cs" />
<Compile Include="Processes\ProcessOutput.cs" />
<Compile Include="RateGate.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);
}
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();

View File

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

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.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);

View File

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

View File

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

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

View File

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

View File

@ -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<Int32> grabbed
);
}
public class MissingEpisodeSearchService : IEpisodeSearchService, IExecute<EpisodeSearchCommand>, IExecute<MissingEpisodeSearchCommand>
@ -37,11 +39,12 @@ namespace NzbDrone.Core.IndexerSearch
_logger = logger;
}
public void MissingEpisodesAiredAfter(DateTime dateTime)
public void MissingEpisodesAiredAfter(DateTime dateTime, IEnumerable<Int32> 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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<XbmcJsonResult<JObject>>(response);
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\})",
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<string>
{
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)

View File

@ -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?(?<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),
//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(@"^(?<title>.+?)(?:(?:_|-|\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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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({

View File

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

View File

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

View File

@ -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 {

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

View File

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

View File

@ -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">

View File

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

View File

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

View File

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

View File

@ -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 = {