1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-03-18 16:24:13 -04:00

Compare commits

..

51 Commits

Author SHA1 Message Date
Taloth Saldono
c886a02388 Warn if users attempt to store database on a network share, but also force database into journal mode. 2018-11-27 22:51:43 +01:00
Kevin Richter
537e4d7c39 Fix Quality Detection with DDP5.1 2018-11-24 11:24:24 +01:00
Taloth Saldono
9f16d9b2fc Fixed: File names and release titles lacking a series title and starting with the Air date.
fixes #2825
2018-11-21 22:02:51 +01:00
Taloth Saldono
ae6d920e2a Updated error message if skyhook and other services respond with html content.
closes #2817
2018-11-14 21:48:56 +01:00
Mark McDowall
0d22f9ec29 Improve logging when rejecting release with unmonitored episodes 2018-11-11 21:11:14 -08:00
Mark McDowall
699076a405 New: Added warning for Download Station that 2FA is not supported
Closes #2451
2018-11-10 16:23:33 -08:00
Jeffrey Neer
df593f486f New: Added priority levels to Join Notifications 2018-11-10 14:51:14 -08:00
Mark McDowall
0d95873a05 New: Parsing french anime releases with single absolute episode number
Closes #2798
2018-11-03 18:42:06 -07:00
Mark McDowall
b20acc9063 Fixed: Sort The A-Team properly in series list 2018-11-03 11:58:00 -07:00
Mark McDowall
70d6d25178 New: Parse names with 1080i as 1080p if they are not RAW HD
Closes #2793
2018-11-03 11:52:02 -07:00
Mark McDowall
196d165432 New: Parse names with FHD as 1080p
Closes #2793
2018-11-03 11:45:34 -07:00
Mark McDowall
bb3ca998fc Restrict 4k parsing to avoid false positives 2018-11-03 11:30:41 -07:00
Mark McDowall
da73221cef Fixed: Handling of poorly formed items when parsing results from indexer 2018-10-24 20:43:52 -07:00
Mark McDowall
36f66eed21 New: Parse names with 4k as 2160p
Closes #2788
2018-10-24 20:13:57 -07:00
Mark McDowall
8e916d60f5 Fixed: Parsing of specials with only season and episode numbers in the file name 2018-10-24 18:32:22 -07:00
Mark McDowall
44048207f2 Remove file quality matches release import spec
New: Don't reject imports when quality doesn't match release quality
New: Reject grab when release was grabbed and imported already
Closes #2783
2018-10-22 20:37:32 -07:00
Mark McDowall
b73b99df8d Fixed: Don't clean Kodi library if Always Update is disabled and video is playing
Fixes #2773
2018-10-22 14:20:22 -07:00
Mark McDowall
ad69ecc5eb Fixed: Use season number from episode instead of parsed from release for custom scripts
Closes #2748
2018-10-07 19:03:32 -07:00
Mark McDowall
1304bc8fb9 Fixed: Exclude /snap/* locations from disk space
Closes #2743
2018-10-07 19:03:32 -07:00
Mark McDowall
a4f63e728c Fixed: Don't use media info for non-video files
Fixes #2745
2018-10-07 19:03:32 -07:00
Jeff Byrnes
307b3536b7 New: Compatibility with Hombrew-installed mono 2018-09-15 10:49:22 -07:00
Mark McDowall
24c6d3f4b3 Don't read response stream if it equals Stream.Null 2018-09-14 17:50:13 -07:00
Mark McDowall
4a052708c8 New: Updated pushover app clone URL 2018-09-04 00:19:09 -07:00
Mark McDowall
39a8d4f0d8 Fixed: Parsing of new hashed release filenames (######_##.ext) 2018-09-03 11:24:48 -07:00
Mark McDowall
ca22a25842 New: Add stopped option for rTorrent 2018-08-28 18:14:55 -07:00
Mark McDowall
ff9a9a5e4d More restrictions when using download client title or folder name for parsing
Fixes #2663
2018-08-27 21:35:03 -07:00
Mark McDowall
3d7c59bc3b New: Add unique IDs to Kodi metadata
Closes #2711
2018-08-27 20:42:32 -07:00
Mark McDowall
63ea1f1afd Fixed: Skip sample check when rescanning series folder 2018-08-19 09:23:34 -07:00
Mark McDowall
baf8f6cca6 Fixed: Parsing multi-episode in square bracket
Fixes #2669
2018-08-18 11:22:51 -07:00
Mark McDowall
c67c7e1b5a More flexible matching some anime releases 2018-08-18 10:56:18 -07:00
Mark McDowall
46d8e5830a Fixed: Concurrent manual imports silently failing 2018-08-18 10:56:18 -07:00
Taloth Saldono
37054673b7 Fixed: Too big eta in qbit api still occurring on official builds. 2018-08-07 19:08:38 +02:00
Mark McDowall
86bc5c5547 Fixed: Parsing of some anime releases with season number in title
Fixes #2684
2018-07-30 19:20:40 -07:00
Taloth Saldono
fc44607c73 Added missing UrlBase validation for SabnzbdSettings. 2018-07-18 07:39:58 +02:00
Taloth Saldono
2a1421f488 Fixed: Skip torrents in Deluge api that don't have hashes.
closes #2566
closes #2567
closes #2664
2018-07-16 19:16:26 +02:00
Nicholas Landriault
d7a054f637 Deluge torrents that don't have a hash are skipped
In some cases torrents in Deluge may not have a hash (ex: https://torguard.net/checkmytorrentipaddress.php). This causes Sonarr to fail when loading the torrents from Deluge with error message: 'Unable to communicate with deluge. Object reference not set to an instance of an object'. This commit simply causes Sonarr to skip over the torrent with the missing hash and continue loading torrents that do have hashes.
2018-07-16 19:11:10 +02:00
Taloth Saldono
9c9ad9aec3 New: Added optional UrlBase to Nzbget and Sabnzbd settings.
ref #1651
2018-07-15 12:24:27 +02:00
Mark McDowall
1467c52e03 Fixed: Multi-file torrents in Vuze with different folder and file names
Fixes #2571
2018-07-11 19:01:41 -07:00
Mark McDowall
e407145d10 Fixed: .vtt files treated as subtitles 2018-07-08 19:22:42 -07:00
Taloth Saldono
476110b1de Fixed: Store BitRate_Nominal (VBR) mediainfo in database instead of only BitRate.
ref Radarr/Radarr#2860
2018-07-08 18:36:36 +02:00
Taloth Saldono
45f9f45f50 Fixed: Quality parser for the rare HD-DVD. 2018-07-07 20:04:45 +02:00
Taloth Saldono
d581d997c2 Fixed: Ignore /etc in System disk overview. 2018-07-07 11:09:04 +02:00
Taloth Saldono
633344e5bb Disabled httpbin.org tests for now due to the site being flaky. 2018-07-06 21:53:03 +02:00
Taloth Saldono
0cce6b74f9 Added logging of json snippets on json deserialization errors. 2018-07-06 21:44:15 +02:00
Taloth Saldono
8b8bfb9bf0 Added third httpbin site. 2018-07-06 19:50:29 +02:00
Taloth Saldono
7241ca4ae9 Run http tests more gracefully. 2018-07-06 19:09:59 +02:00
Taloth Saldono
e9b11e55e9 Fixed: Regression with importing nested obfuscated directories.
Closes #2640
2018-07-04 21:55:08 +02:00
Mark McDowall
48126f55ed Fixed: Parsing titles with question marks
Fixes #2637
2018-06-27 19:54:25 -07:00
Mark McDowall
cb549507ee Fixed: Parsing dates using underscores for separation 2018-06-27 18:32:28 -07:00
Mark McDowall
a0b6cdb08e Fixed: Forced seeding in QBittorrent status treated as complete 2018-06-27 18:31:44 -07:00
Taloth Saldono
9b9597093c Fixed: Regression causing Manual Import to ignore user provided information. 2018-06-21 07:23:20 +02:00
84 changed files with 1126 additions and 398 deletions

View File

@@ -52,15 +52,6 @@ CleanFolder()
find $path -depth -empty -type d -exec rm -r "{}" \;
}
AddJsonNet()
{
rm $outputFolder/Newtonsoft.Json.*
cp $sourceFolder/packages/Newtonsoft.Json.*/lib/net35/*.dll $outputFolder
cp $sourceFolder/packages/Newtonsoft.Json.*/lib/net35/*.dll $outputFolder/NzbDrone.Update
}
BuildWithMSBuild()
{
export PATH=$msBuild:$PATH
@@ -91,8 +82,6 @@ Build()
CleanFolder $outputFolder false
AddJsonNet
echo "Removing Mono.Posix.dll"
rm $outputFolder/Mono.Posix.dll

View File

@@ -9,7 +9,11 @@ APPNAME="Sonarr"
#set up environment
if [[ -x '/opt/local/bin/mono' ]]; then
# Macports and mono-supplied installer path
export PATH="/opt/local/bin:$PATH"
elif [[ -x '/usr/local/bin/mono' ]]; then
# Homebrew-supplied path to mono
export PATH="/usr/local/bin:$PATH"
fi
export DYLD_FALLBACK_LIBRARY_PATH="$DIR"

View File

@@ -13,6 +13,7 @@ namespace NzbDrone.Api.ManualImport
{
public string Path { get; set; }
public string RelativePath { get; set; }
public string FolderName { get; set; }
public string Name { get; set; }
public long Size { get; set; }
public SeriesResource Series { get; set; }
@@ -36,6 +37,7 @@ namespace NzbDrone.Api.ManualImport
Path = model.Path,
RelativePath = model.RelativePath,
FolderName = model.FolderName,
Name = model.Name,
Size = model.Size,
Series = model.Series.ToResource(),

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading;
using FluentAssertions;
@@ -24,13 +25,60 @@ namespace NzbDrone.Common.Test.Http
[TestFixture(typeof(CurlHttpDispatcher))]
public class HttpClientFixture<TDispatcher> : TestBase<HttpClient> where TDispatcher : IHttpDispatcher
{
private static string[] _httpBinHosts = new[] { "eu.httpbin.org", "httpbin.org" };
private static int _httpBinRandom;
private string[] _httpBinHosts;
private int _httpBinSleep;
private int _httpBinRandom;
private string _httpBinHost;
private string _httpBinHost2;
[OneTimeSetUp]
public void FixtureSetUp()
{
var candidates = new[] { "eu.httpbin.org", /*"httpbin.org",*/ "www.httpbin.org" };
// httpbin.org is broken right now, occassionally redirecting to https if it's unavailable.
_httpBinHosts = candidates.Where(IsTestSiteAvailable).ToArray();
TestLogger.Info($"{candidates.Length} TestSites available.");
_httpBinSleep = _httpBinHosts.Count() < 2 ? 100 : 10;
}
private bool IsTestSiteAvailable(string site)
{
try
{
var req = WebRequest.Create($"http://{site}/get") as HttpWebRequest;
var res = req.GetResponse() as HttpWebResponse;
if (res.StatusCode != HttpStatusCode.OK) return false;
try
{
req = WebRequest.Create($"http://{site}/status/429") as HttpWebRequest;
res = req.GetResponse() as HttpWebResponse;
}
catch (WebException ex)
{
res = ex.Response as HttpWebResponse;
}
if (res == null || res.StatusCode != (HttpStatusCode)429) return false;
return true;
}
catch
{
return false;
}
}
[SetUp]
public void SetUp()
{
if (!_httpBinHosts.Any())
{
Assert.Inconclusive("No TestSites available");
}
Mocker.GetMock<IPlatformInfo>().Setup(c => c.Version).Returns(new Version("1.0.0"));
Mocker.GetMock<IOsInfo>().Setup(c => c.Name).Returns("TestOS");
Mocker.GetMock<IOsInfo>().Setup(c => c.Version).Returns("9.0.0");
@@ -50,6 +98,13 @@ namespace NzbDrone.Common.Test.Http
// Roundrobin over the two servers, to reduce the chance of hitting the ratelimiter.
_httpBinHost = _httpBinHosts[_httpBinRandom++ % _httpBinHosts.Length];
_httpBinHost2 = _httpBinHosts[_httpBinRandom % _httpBinHosts.Length];
}
[TearDown]
public void TearDown()
{
Thread.Sleep(_httpBinSleep);
}
[Test]
@@ -245,7 +300,12 @@ namespace NzbDrone.Common.Test.Http
public void GivenOldCookie()
{
var oldRequest = new HttpRequest("http://eu.httpbin.org/get");
if (_httpBinHost == _httpBinHost2)
{
Assert.Inconclusive("Need both httpbin.org and eu.httpbin.org to run this test.");
}
var oldRequest = new HttpRequest($"http://{_httpBinHost2}/get");
oldRequest.Cookies["my"] = "cookie";
var oldClient = new HttpClient(new IHttpRequestInterceptor[0], Mocker.Resolve<ICacheManager>(), Mocker.Resolve<IRateLimitService>(), Mocker.Resolve<IHttpDispatcher>(), Mocker.GetMock<IUserAgentBuilder>().Object, Mocker.Resolve<Logger>());
@@ -262,7 +322,7 @@ namespace NzbDrone.Common.Test.Http
{
GivenOldCookie();
var request = new HttpRequest("http://eu.httpbin.org/get");
var request = new HttpRequest($"http://{_httpBinHost2}/get");
var response = Subject.Get<HttpBinResource>(request);
@@ -278,7 +338,7 @@ namespace NzbDrone.Common.Test.Http
{
GivenOldCookie();
var request = new HttpRequest("http://httpbin.org/get");
var request = new HttpRequest($"http://{_httpBinHost}/get");
var response = Subject.Get<HttpBinResource>(request);

View File

@@ -1,4 +1,5 @@
using System;
using System.IO;
using System.Net;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
@@ -101,7 +102,7 @@ namespace NzbDrone.Common.Http.Dispatchers
using (var responseStream = httpWebResponse.GetResponseStream())
{
if (responseStream != null)
if (responseStream != null && responseStream != Stream.Null)
{
try
{

View File

@@ -267,6 +267,7 @@ namespace NzbDrone.Common.Http
public HttpResponse<T> Get<T>(HttpRequest request) where T : new()
{
var response = Get(request);
CheckResponseContentType(response);
return new HttpResponse<T>(response);
}
@@ -285,7 +286,16 @@ namespace NzbDrone.Common.Http
public HttpResponse<T> Post<T>(HttpRequest request) where T : new()
{
var response = Post(request);
CheckResponseContentType(response);
return new HttpResponse<T>(response);
}
private void CheckResponseContentType(HttpResponse response)
{
if (response.Headers.ContentType != null && response.Headers.ContentType.Contains("text/html"))
{
throw new UnexpectedHtmlContentException(response);
}
}
}
}

View File

@@ -7,13 +7,19 @@ namespace NzbDrone.Common.Http
public HttpRequest Request { get; private set; }
public HttpResponse Response { get; private set; }
public HttpException(HttpRequest request, HttpResponse response)
: base(string.Format("HTTP request failed: [{0}:{1}] [{2}] at [{3}]", (int)response.StatusCode, response.StatusCode, request.Method, request.Url))
public HttpException(HttpRequest request, HttpResponse response, string message)
: base(message)
{
Request = request;
Response = response;
}
public HttpException(HttpRequest request, HttpResponse response)
: this(request, response, string.Format("HTTP request failed: [{0}:{1}] [{2}] at [{3}]", (int)response.StatusCode, response.StatusCode, request.Method, request.Url))
{
}
public HttpException(HttpResponse response)
: this(response.Request, response)
{
@@ -30,4 +36,4 @@ namespace NzbDrone.Common.Http
return base.ToString();
}
}
}
}

View File

@@ -135,7 +135,7 @@ namespace NzbDrone.Common.Http
return new HttpUri(Scheme, Host, Port, CombinePath(Path, path), Query, Fragment);
}
private static string CombinePath(string basePath, string relativePath)
public static string CombinePath(string basePath, string relativePath)
{
if (relativePath.IsNullOrWhiteSpace())
{

View File

@@ -0,0 +1,13 @@
using System;
namespace NzbDrone.Common.Http
{
public class UnexpectedHtmlContentException : HttpException
{
public UnexpectedHtmlContentException(HttpResponse response)
: base(response.Request, response, $"Site responded with browser content instead of api data. This disruption may be temporary, please try again later. [{response.Request.Url}]")
{
}
}
}

View File

@@ -177,6 +177,7 @@
<Compile Include="Http\HttpRequestBuilderFactory.cs" />
<Compile Include="Http\Proxy\ProxyType.cs" />
<Compile Include="Http\TlsFailureException.cs" />
<Compile Include="Http\UnexpectedHtmlContentException.cs" />
<Compile Include="Http\TooManyRequestsException.cs" />
<Compile Include="Extensions\IEnumerableExtensions.cs" />
<Compile Include="Http\UserAgentBuilder.cs" />

View File

@@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Reflection;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
@@ -14,13 +15,13 @@ namespace NzbDrone.Common.Serializer
static Json()
{
SerializerSetting = new JsonSerializerSettings
{
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
NullValueHandling = NullValueHandling.Ignore,
Formatting = Formatting.Indented,
DefaultValueHandling = DefaultValueHandling.Include,
ContractResolver = new CamelCasePropertyNamesContractResolver()
};
{
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
NullValueHandling = NullValueHandling.Ignore,
Formatting = Formatting.Indented,
DefaultValueHandling = DefaultValueHandling.Include,
ContractResolver = new CamelCasePropertyNamesContractResolver()
};
SerializerSetting.Converters.Add(new StringEnumConverter { CamelCaseText = true });
@@ -34,12 +35,61 @@ namespace NzbDrone.Common.Serializer
public static T Deserialize<T>(string json) where T : new()
{
return JsonConvert.DeserializeObject<T>(json, SerializerSetting);
try
{
return JsonConvert.DeserializeObject<T>(json, SerializerSetting);
}
catch (JsonReaderException ex)
{
throw DetailedJsonReaderException(ex, json);
}
}
public static object Deserialize(string json, Type type)
{
return JsonConvert.DeserializeObject(json, type, SerializerSetting);
try
{
return JsonConvert.DeserializeObject(json, type, SerializerSetting);
}
catch (JsonReaderException ex)
{
throw DetailedJsonReaderException(ex, json);
}
}
private static JsonReaderException DetailedJsonReaderException(JsonReaderException ex, string json)
{
var lineNumber = ex.LineNumber == 0 ? 0 : (ex.LineNumber - 1);
var linePosition = ex.LinePosition;
var lines = json.Split('\n');
if (lineNumber >= 0 && lineNumber < lines.Length &&
linePosition >= 0 && linePosition < lines[lineNumber].Length)
{
var line = lines[lineNumber];
var start = Math.Max(0, linePosition - 20);
var end = Math.Min(line.Length, linePosition + 20);
var snippetBefore = line.Substring(start, linePosition - start);
var snippetAfter = line.Substring(linePosition, end - linePosition);
var message = ex.Message + " (Json snippet '" + snippetBefore + "<--error-->" + snippetAfter + "')";
// Not risking updating JSON.net from 9.x to 10.x just to get this as public ctor.
var ctor = typeof(JsonReaderException).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[] { typeof(string), typeof(Exception), typeof(string), typeof(int), typeof(int) }, null);
if (ctor != null)
{
return (JsonReaderException)ctor.Invoke(new object[] { message, ex, ex.Path, ex.LineNumber, linePosition });
}
// JSON.net 10.x ctor in case we update later.
ctor = typeof(JsonReaderException).GetConstructor(BindingFlags.Public | BindingFlags.Instance, null, new Type[] { typeof(string), typeof(string), typeof(int), typeof(int), typeof(Exception) }, null);
if (ctor != null)
{
return (JsonReaderException)ctor.Invoke(new object[] { message, ex.Path, ex.LineNumber, linePosition, ex });
}
}
return ex;
}
public static bool TryDeserialize<T>(string json, out T result) where T : new()
@@ -78,4 +128,4 @@ namespace NzbDrone.Common.Serializer
Serialize(model, new StreamWriter(outputStream));
}
}
}
}

View File

@@ -0,0 +1,163 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.History;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Tv;
using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.DecisionEngineTests
{
[TestFixture]
public class AlreadyImportedSpecificationFixture : CoreTest<AlreadyImportedSpecification>
{
private const int FIRST_EPISODE_ID = 1;
private const string TITLE = "Series.Title.S01E01.720p.HDTV.x264-Sonarr";
private Series _series;
private QualityModel _hdtv720p;
private QualityModel _hdtv1080p;
private RemoteEpisode _remoteEpisode;
private List<History.History> _history;
[SetUp]
public void Setup()
{
var singleEpisodeList = new List<Episode>
{
new Episode
{
Id = FIRST_EPISODE_ID,
SeasonNumber = 12,
EpisodeNumber = 3,
EpisodeFileId = 1
}
};
_series = Builder<Series>.CreateNew()
.Build();
_hdtv720p = new QualityModel(Quality.HDTV720p, new Revision(version: 1));
_hdtv1080p = new QualityModel(Quality.HDTV1080p, new Revision(version: 1));
_remoteEpisode = new RemoteEpisode
{
Series = _series,
ParsedEpisodeInfo = new ParsedEpisodeInfo { Quality = _hdtv720p },
Episodes = singleEpisodeList,
Release = Builder<ReleaseInfo>.CreateNew()
.Build()
};
_history = new List<History.History>();
Mocker.GetMock<IConfigService>()
.SetupGet(s => s.EnableCompletedDownloadHandling)
.Returns(true);
Mocker.GetMock<IHistoryService>()
.Setup(s => s.FindByEpisodeId(It.IsAny<int>()))
.Returns(_history);
}
private void GivenCdhDisabled()
{
Mocker.GetMock<IConfigService>()
.SetupGet(s => s.EnableCompletedDownloadHandling)
.Returns(false);
}
private void GivenHistoryItem(string downloadId, string sourceTitle, QualityModel quality, HistoryEventType eventType)
{
_history.Add(new History.History
{
DownloadId = downloadId,
SourceTitle = sourceTitle,
Quality = quality,
Date = DateTime.UtcNow,
EventType = eventType
});
}
[Test]
public void should_be_accepted_if_CDH_is_disabled()
{
GivenCdhDisabled();
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
}
[Test]
public void should_be_accepted_if_episode_does_not_have_a_file()
{
_remoteEpisode.Episodes.First().EpisodeFileId = 0;
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
}
[Test]
public void should_be_accepted_if_episode_does_not_have_grabbed_event()
{
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
}
[Test]
public void should_be_accepted_if_episode_does_not_have_imported_event()
{
GivenHistoryItem(Guid.NewGuid().ToString().ToUpper(), TITLE, _hdtv720p, HistoryEventType.Grabbed);
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
}
[Test]
public void should_be_accepted_if_grabbed_and_imported_quality_is_the_same()
{
var downloadId = Guid.NewGuid().ToString().ToUpper();
GivenHistoryItem(downloadId, TITLE, _hdtv720p, HistoryEventType.Grabbed);
GivenHistoryItem(downloadId, TITLE, _hdtv720p, HistoryEventType.DownloadFolderImported);
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
}
[Test]
public void should_be_rejected_if_grabbed_download_id_matches_release_torrent_hash()
{
var downloadId = Guid.NewGuid().ToString().ToUpper();
GivenHistoryItem(downloadId, TITLE, _hdtv720p, HistoryEventType.Grabbed);
GivenHistoryItem(downloadId, TITLE, _hdtv1080p, HistoryEventType.DownloadFolderImported);
_remoteEpisode.Release = Builder<TorrentInfo>.CreateNew()
.With(t => t.DownloadProtocol = DownloadProtocol.Torrent)
.With(t => t.InfoHash = downloadId)
.Build();
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse();
}
[Test]
public void should_be_rejected_if_release_title_matches_grabbed_event_source_title()
{
var downloadId = Guid.NewGuid().ToString().ToUpper();
GivenHistoryItem(downloadId, TITLE, _hdtv720p, HistoryEventType.Grabbed);
GivenHistoryItem(downloadId, TITLE, _hdtv1080p, HistoryEventType.DownloadFolderImported);
_remoteEpisode.Release = Builder<TorrentInfo>.CreateNew()
.With(t => t.DownloadProtocol = DownloadProtocol.Torrent)
.With(t => t.InfoHash = downloadId)
.Build();
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse();
}
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using FizzWare.NBuilder;
using FluentAssertions;
@@ -16,7 +16,7 @@ using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.DecisionEngineTests
namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
{
[TestFixture]
public class HistorySpecificationFixture : CoreTest<HistorySpecification>

View File

@@ -137,6 +137,7 @@ namespace NzbDrone.Core.Test.DiskSpace
[TestCase("/var/lib/kubelet")]
[TestCase("/var/lib/docker")]
[TestCase("/some/place/docker/aufs")]
[TestCase("/etc/network")]
public void should_not_check_diskspace_for_irrelevant_mounts(string path)
{
var mount = new Mock<IMount>();

View File

@@ -290,6 +290,24 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DelugeTests
item.CanBeRemoved.Should().Be(canBeRemoved);
}
[Test]
public void GetItems_should_ignore_items_without_hash()
{
_downloading.Hash = null;
GivenTorrents(new List<DelugeTorrent>
{
_downloading,
_queued
});
var items = Subject.GetItems();
items.Should().HaveCount(1);
items.First().Status.Should().Be(DownloadItemStatus.Queued);
}
[Test]
public void should_return_status_with_outputdirs()
{

View File

@@ -162,6 +162,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
[TestCase("uploading")]
[TestCase("stalledUP")]
[TestCase("checkingUP")]
[TestCase("forcedUP")]
public void completed_item_should_have_required_properties(string state)
{
var torrent = new QBittorrentTorrent
@@ -494,5 +495,18 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
var item = Subject.GetItems().Single();
item.Category.Should().Be(category);
}
[Test]
public void should_handle_eta_biginteger()
{
// Let this stand as a lesson to never write temporary unit tests on your dev machine and claim it works.
// Commit the tests and let it run with the official build on the official build agents.
// (Also don't replace library versions in your build script)
var json = "{ \"eta\": 18446744073709335000 }";
var torrent = Newtonsoft.Json.JsonConvert.DeserializeObject<QBittorrentTorrent>(json);
torrent.Eta.ToString().Should().Be("18446744073709335000");
}
}
}

View File

@@ -3,6 +3,7 @@ using System.Linq;
using FizzWare.NBuilder;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
@@ -105,5 +106,47 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators
Mocker.GetMock<IParsingService>()
.Verify(v => v.GetEpisodes(folderEpisodeInfo, _series, localEpisode.SceneSource, null), Times.Once());
}
[Test]
public void should_use_file_when_folder_is_absolute_and_file_is_not()
{
var fileEpisodeInfo = Parser.Parser.ParseTitle("Series.Title.S01E01");
var folderEpisodeInfo = Parser.Parser.ParseTitle("Series.Title.01");
var localEpisode = new LocalEpisode
{
FileEpisodeInfo = fileEpisodeInfo,
FolderEpisodeInfo = folderEpisodeInfo,
Path = @"C:\Test\Unsorted TV\Series.Title.101\Series.Title.S01E01.mkv".AsOsAgnostic(),
Series = _series
};
Subject.Aggregate(localEpisode, false);
Mocker.GetMock<IParsingService>()
.Verify(v => v.GetEpisodes(fileEpisodeInfo, _series, localEpisode.SceneSource, null), Times.Once());
}
[Test]
public void should_use_special_info_when_not_null()
{
var fileEpisodeInfo = Parser.Parser.ParseTitle("S00E01");
var specialEpisodeInfo = fileEpisodeInfo.JsonClone();
var localEpisode = new LocalEpisode
{
FileEpisodeInfo = fileEpisodeInfo,
Path = @"C:\Test\TV\Series\Specials\S00E01.mkv".AsOsAgnostic(),
Series = _series
};
Mocker.GetMock<IParsingService>()
.Setup(s => s.ParseSpecialEpisodeTitle(fileEpisodeInfo, It.IsAny<string>(), _series))
.Returns(specialEpisodeInfo);
Subject.Aggregate(localEpisode, false);
Mocker.GetMock<IParsingService>()
.Verify(v => v.GetEpisodes(specialEpisodeInfo, _series, localEpisode.SceneSource, null), Times.Once());
}
}
}

View File

@@ -87,7 +87,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
private void GivenAugmentationSuccess()
{
Mocker.GetMock<IAugmentingService>()
Mocker.GetMock<IAggregationService>()
.Setup(s => s.Augment(It.IsAny<LocalEpisode>(), It.IsAny<bool>()))
.Callback<LocalEpisode, bool>((localEpisode, otherFiles) =>
{
@@ -158,7 +158,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
{
GivenSpecifications(_pass1);
Mocker.GetMock<IAugmentingService>()
Mocker.GetMock<IAggregationService>()
.Setup(c => c.Augment(It.IsAny<LocalEpisode>(), It.IsAny<bool>()))
.Throws<TestException>();
@@ -173,7 +173,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
Subject.GetImportDecisions(_videoFiles, _series);
Mocker.GetMock<IAugmentingService>()
Mocker.GetMock<IAggregationService>()
.Verify(c => c.Augment(It.IsAny<LocalEpisode>(), It.IsAny<bool>()), Times.Exactly(_videoFiles.Count));
ExceptionVerification.ExpectedErrors(3);
@@ -195,7 +195,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
var decisions = Subject.GetImportDecisions(_videoFiles, _series);
Mocker.GetMock<IAugmentingService>()
Mocker.GetMock<IAggregationService>()
.Verify(c => c.Augment(It.IsAny<LocalEpisode>(), It.IsAny<bool>()), Times.Exactly(_videoFiles.Count));
decisions.Should().HaveCount(3);
@@ -205,7 +205,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
[Test]
public void should_return_a_decision_when_exception_is_caught()
{
Mocker.GetMock<IAugmentingService>()
Mocker.GetMock<IAggregationService>()
.Setup(c => c.Augment(It.IsAny<LocalEpisode>(), It.IsAny<bool>()))
.Throws<TestException>();

View File

@@ -29,6 +29,13 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
};
}
[Test]
public void should_return_true_if_no_fileinfo_available()
{
_localEpisode.FileEpisodeInfo = null;
Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeTrue();
}
[Test]
public void should_return_false_when_file_contains_the_full_season()
{

View File

@@ -1,123 +0,0 @@
using System.Collections.Generic;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.Download;
using NzbDrone.Core.History;
using NzbDrone.Core.MediaFiles.EpisodeImport.Specifications;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
{
[TestFixture]
public class GrabbedReleaseQualityFixture : CoreTest<GrabbedReleaseQualitySpecification>
{
private LocalEpisode _localEpisode;
private DownloadClientItem _downloadClientItem;
[SetUp]
public void Setup()
{
_localEpisode = Builder<LocalEpisode>.CreateNew()
.With(l => l.Quality = new QualityModel(Quality.Bluray720p))
.Build();
_downloadClientItem = Builder<DownloadClientItem>.CreateNew()
.Build();
}
private void GivenHistory(List<History.History> history)
{
Mocker.GetMock<IHistoryService>()
.Setup(s => s.FindByDownloadId(It.IsAny<string>()))
.Returns(history);
}
[Test]
public void should_be_accepted_when_downloadClientItem_is_null()
{
Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeTrue();
}
[Test]
public void should_be_accepted_if_no_history_for_downloadId()
{
GivenHistory(new List<History.History>());
Subject.IsSatisfiedBy(_localEpisode, _downloadClientItem).Accepted.Should().BeTrue();
}
[Test]
public void should_be_accepted_if_no_grabbed_history_for_downloadId()
{
var history = Builder<History.History>.CreateListOfSize(1)
.All()
.With(h => h.EventType = HistoryEventType.Unknown)
.BuildList();
GivenHistory(history);
Subject.IsSatisfiedBy(_localEpisode, _downloadClientItem).Accepted.Should().BeTrue();
}
[Test]
public void should_be_accepted_if_grabbed_history_is_for_a_season_pack()
{
var history = Builder<History.History>.CreateListOfSize(1)
.All()
.With(h => h.EventType = HistoryEventType.Grabbed)
.With(h => h.Quality = _localEpisode.Quality)
.With(h => h.SourceTitle = "Series.Title.S01.720p.HDTV.x264-RlsGroup")
.BuildList();
GivenHistory(history);
Subject.IsSatisfiedBy(_localEpisode, _downloadClientItem).Accepted.Should().BeTrue();
}
[Test]
public void should_be_accepted_if_grabbed_history_quality_is_unknown()
{
var history = Builder<History.History>.CreateListOfSize(1)
.All()
.With(h => h.EventType = HistoryEventType.Grabbed)
.With(h => h.Quality = new QualityModel(Quality.Unknown))
.BuildList();
GivenHistory(history);
Subject.IsSatisfiedBy(_localEpisode, _downloadClientItem).Accepted.Should().BeTrue();
}
[Test]
public void should_be_accepted_if_grabbed_history_quality_matches()
{
var history = Builder<History.History>.CreateListOfSize(1)
.All()
.With(h => h.EventType = HistoryEventType.Grabbed)
.With(h => h.Quality = _localEpisode.Quality)
.BuildList();
GivenHistory(history);
Subject.IsSatisfiedBy(_localEpisode, _downloadClientItem).Accepted.Should().BeTrue();
}
[Test]
public void should_be_rejected_if_grabbed_history_quality_does_not_match()
{
var history = Builder<History.History>.CreateListOfSize(1)
.All()
.With(h => h.EventType = HistoryEventType.Grabbed)
.With(h => h.Quality = new QualityModel(Quality.HDTV720p))
.BuildList();
GivenHistory(history);
Subject.IsSatisfiedBy(_localEpisode, _downloadClientItem).Accepted.Should().BeFalse();
}
}
}

View File

@@ -50,6 +50,15 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeTrue();
}
[Test]
public void should_be_accepted_if_file_name_is_not_parseable()
{
_localEpisode.Path = @"C:\Test\Unsorted\Series.Title.S01E01\AFDAFD.mkv".AsOsAgnostic();
_localEpisode.FileEpisodeInfo = null;
Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeTrue();
}
[Test]
public void should_should_be_accepted_for_full_season()
{

View File

@@ -1,17 +1,29 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.IndexerSearch;
using NzbDrone.Core.MediaFiles.Commands;
using NzbDrone.Core.MediaFiles.EpisodeImport.Manual;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Update.Commands;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.Messaging.Commands
{
[TestFixture]
public class CommandEqualityComparerFixture
{
private string GivenRandomPath()
{
return Path.Combine(@"C:\Tesst\", Guid.NewGuid().ToString()).AsOsAgnostic();
}
[Test]
public void should_return_true_when_there_are_no_properties()
{
@@ -107,5 +119,43 @@ namespace NzbDrone.Core.Test.Messaging.Commands
CommandEqualityComparer.Instance.Equals(command1, command2).Should().BeFalse();
}
[Test]
public void should_return_true_when_commands_list_for_non_primitive_type_match()
{
var files1 = Builder<ManualImportFile>.CreateListOfSize(2)
.All()
.With(m => m.Path = GivenRandomPath())
.Build()
.ToList();
var files2 = files1.JsonClone();
var command1 = new ManualImportCommand { Files = files1 };
var command2 = new ManualImportCommand { Files = files2 };
CommandEqualityComparer.Instance.Equals(command1, command2).Should().BeTrue();
}
[Test]
public void should_return_false_when_commands_list_for_non_primitive_type_dont_match()
{
var files1 = Builder<ManualImportFile>.CreateListOfSize(2)
.All()
.With(m => m.Path = GivenRandomPath())
.Build()
.ToList();
var files2 = Builder<ManualImportFile>.CreateListOfSize(2)
.All()
.With(m => m.Path = GivenRandomPath())
.Build()
.ToList();
var command1 = new ManualImportCommand { Files = files1 };
var command2 = new ManualImportCommand { Files = files2 };
CommandEqualityComparer.Instance.Equals(command1, command2).Should().BeFalse();
}
}
}

View File

@@ -166,7 +166,8 @@
<Compile Include="DecisionEngineTests\ProtocolSpecificationFixture.cs" />
<Compile Include="DecisionEngineTests\CutoffSpecificationFixture.cs" />
<Compile Include="DecisionEngineTests\DownloadDecisionMakerFixture.cs" />
<Compile Include="DecisionEngineTests\HistorySpecificationFixture.cs" />
<Compile Include="DecisionEngineTests\AlreadyImportedSpecificationFixture.cs" />
<Compile Include="DecisionEngineTests\RssSync\HistorySpecificationFixture.cs" />
<Compile Include="DecisionEngineTests\LanguageSpecificationFixture.cs" />
<Compile Include="DecisionEngineTests\MonitoredEpisodeSpecificationFixture.cs" />
<Compile Include="DecisionEngineTests\QueueSpecificationFixture.cs" />
@@ -316,7 +317,6 @@
<Compile Include="MediaFiles\EpisodeImport\Specifications\FreeSpaceSpecificationFixture.cs" />
<Compile Include="MediaFiles\EpisodeImport\Specifications\FullSeasonSpecificationFixture.cs" />
<Compile Include="MediaFiles\EpisodeImport\Specifications\SameFileSpecificationFixture.cs" />
<Compile Include="MediaFiles\EpisodeImport\Specifications\GrabbedReleaseQualityFixture.cs" />
<Compile Include="MediaFiles\EpisodeImport\Specifications\MatchesFolderSpecificationFixture.cs" />
<Compile Include="MediaFiles\EpisodeImport\Specifications\NotSampleSpecificationFixture.cs" />
<Compile Include="MediaFiles\EpisodeImport\Specifications\NotUnpackingSpecificationFixture.cs" />

View File

@@ -89,6 +89,11 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Love Rerun EP06 720p x265 AOZ.mp4", "Love Rerun", 6, 0, 0)]
[TestCase("Love Rerun 2018 EP06 720p x265 AOZ.mp4", "Love Rerun 2018", 6, 0, 0)]
[TestCase("Love Rerun 2018 06 720p x265 AOZ.mp4", "Love Rerun 2018", 6, 0, 0)]
[TestCase("Boku No Hero Academia S03 - EP14 VOSTFR [1080p] [HardSub] Yass'Kun", "Boku No Hero Academia S03", 14, 0, 0)]
[TestCase("Boku No Hero Academia S3 - 15 VOSTFR [720p]", "Boku No Hero Academia S3", 15, 0, 0)]
[TestCase("Tokyo Ghoul: RE S2 - Episode 4 VOSTFR (1080p)", "Tokyo Ghoul RE S2", 4, 0, 0)]
[TestCase("To Aru Majutsu no Index III - Episode 5 VOSTFR (1080p)", "To Aru Majutsu no Index III", 5, 0, 0)]
[TestCase("[Prout] Steins;Gate 0 - Episode 5 VOSTFR (BDRip 1920x1080 x264 FLAC)", "Steins;Gate 0", 5, 0, 0)]
//[TestCase("", "", 0, 0, 0)]
public void should_parse_absolute_numbers(string postTitle, string title, int absoluteEpisodeNumber, int seasonNumber, int episodeNumber)
{

View File

@@ -27,6 +27,9 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("At_Midnight_140722_720p_HDTV_x264-YesTV", "At Midnight", 2014, 07, 22)]
//[TestCase("Corrie.07.01.15", "Corrie", 2015, 1, 7)]
[TestCase("The Nightly Show with Larry Wilmore 2015 02 09 WEBRIP s01e13", "The Nightly Show with Larry Wilmore", 2015, 2, 9)]
[TestCase("Jimmy_Fallon_2018_06_22_Seth_Meyers_720p_HEVC_x265-MeGusta", "Jimmy Fallon", 2018, 6, 22)]
[TestCase("20161024- Exotic Payback.21x41_720.mkv", "", 2016, 10, 24)]
[TestCase("2018-11-14.1080.all.mp4", "", 2018, 11, 14)]
//[TestCase("", "", 0, 0, 0)]
public void should_parse_daily_episode(string postTitle, string title, int year, int month, int day)
{

View File

@@ -1,4 +1,4 @@
using FluentAssertions;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Test.Framework;
@@ -80,6 +80,13 @@ namespace NzbDrone.Core.Test.ParserTests
"The Good Wife",
Quality.HDTV720p,
"NZBgeek"
},
new object[]
{
@"C:\Test\Fargo.S03E04.1080p.WEB-DL.DD5.1.H264-RARBG\170424_26.mkv".AsOsAgnostic(),
"Fargo",
Quality.WEBDL1080p,
"RARBG"
}
};

View File

@@ -60,6 +60,8 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("S01E01-E03 - Episode Title.HDTV-720p", "", 1, new [] { 1, 2, 3 })]
[TestCase("1x01-x03 - Episode Title.HDTV-720p", "", 1, new [] { 1, 2, 3 })]
[TestCase("Are.You.Human.Too.E07-E08.180612.1080p-NEXT", "Are You Human Too", 1, new[] { 7, 8 })]
[TestCase("Are You Human Too? E11-E12 1080p HDTV AAC H.264-NEXT", "Are You Human Too", 1, new[] { 11, 12 })]
[TestCase("The Series Title (2010) - [S01E01-02-03] - Episode Title", "The Series Title (2010)", 1, new [] { 1, 2, 3 })]
//[TestCase("", "", , new [] { })]
public void should_parse_multiple_episodes(string postTitle, string title, int season, int[] episodes)
{

View File

@@ -118,6 +118,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Hells.Kitchen.US.S12E17.HR.WS.PDTV.X264-DIMENSION", false)]
[TestCase("Survivorman.The.Lost.Pilots.Summer.HR.WS.PDTV.x264-DHD", false)]
[TestCase("Victoria S01E07 - Motor zmen (CZ)[TvRip][HEVC][720p]", false)]
[TestCase("flashpoint.S05E06.720p.HDTV.x264-FHD", false)]
public void should_parse_hdtv720p_quality(string title, bool proper)
{
ParseAndVerifyQuality(title, Quality.HDTV720p, proper);
@@ -130,11 +131,27 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Dexter - S01E01 - Title [HDTV-1080p]", false)]
[TestCase("[HorribleSubs] Yowamushi Pedal - 32 [1080p]", false)]
[TestCase("Victoria S01E07 - Motor zmen (CZ)[TvRip][HEVC][1080p]", false)]
[TestCase("Sword Art Online Alicization 04 vostfr FHD", false)]
[TestCase("Goblin Slayer 04 vostfr FHD.mkv", false)]
[TestCase("[Onii-ChanSub] SSSS.Gridman - 02 vostfr (FHD 1080p 10bits).mkv", false)]
[TestCase("[Miaou] Akanesasu Shoujo 02 VOSTFR FHD 10 bits", false)]
[TestCase("[mhastream.com]_Episode_05_FHD.mp4", false)]
[TestCase("[Kousei]_One_Piece_ - _609_[FHD][648A87C7].mp4", false)]
[TestCase("Presunto culpable 1x02 Culpabilidad [HDTV 1080i AVC MP2 2.0 Sub][GrupoHDS]", false)]
[TestCase("Cuéntame cómo pasó - 19x15 [344] Cuarenta años de baile [HDTV 1080i AVC MP2 2.0 Sub][GrupoHDS]", false)]
public void should_parse_hdtv1080p_quality(string title, bool proper)
{
ParseAndVerifyQuality(title, Quality.HDTV1080p, proper);
}
[TestCase("My Title - S01E01 - EpTitle [HEVC 4k DTSHD-MA-6ch]", false)]
[TestCase("My Title - S01E01 - EpTitle [HEVC-4k DTSHD-MA-6ch]", false)]
[TestCase("My Title - S01E01 - EpTitle [4k HEVC DTSHD-MA-6ch]", false)]
public void should_parse_hdtv2160p_quality(string title, bool proper)
{
ParseAndVerifyQuality(title, Quality.HDTV2160p, proper);
}
[TestCase("Arrested.Development.S04E01.720p.WEBRip.AAC2.0.x264-NFRiP", false)]
[TestCase("Vanguard S01E04 Mexicos Death Train 720p WEB DL", false)]
[TestCase("Hawaii Five 0 S02E21 720p WEB DL DD5 1 H 264", false)]
@@ -178,6 +195,8 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Incorporated.S01E08.Das.geloeschte.Ich.German.DD51.Dubbed.DL.1080p.AmazonHD.x264-TVS", false)]
[TestCase("Death.Note.2017.German.DD51.DL.1080p.NetflixHD.x264-TVS", false)]
[TestCase("Played.S01E08.Pro.Gamer.1440p.BKPL.WEB-DL.H.264-LiGHT", false)]
[TestCase("Good.Luck.Charlie.S04E11.Teddy's.Choice.FHD.1080p.Web-DL", false)]
[TestCase("Outlander.S04E03.The.False.Bride.1080p.NF.WEB.DDP5.1.x264-NTb[rartv]", false)]
public void should_parse_webdl1080p_quality(string title, bool proper)
{
ParseAndVerifyQuality(title, Quality.WEBDL1080p, proper);
@@ -210,6 +229,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("[Elysium]Lucky.Star.01(BD.720p.AAC.DA)[0BB96AD8].mkv", false)]
[TestCase("Battlestar.Galactica.S01E01.33.720p.HDDVD.x264-SiNNERS.mkv", false)]
[TestCase("The.Expanse.S01E07.RERIP.720p.BluRay.x264-DEMAND", true)]
[TestCase("Sans.Laisser.De.Traces.FRENCH.720p.BluRay.x264-FHD", false)]
public void should_parse_bluray720p_quality(string title, bool proper)
{
ParseAndVerifyQuality(title, Quality.Bluray720p, proper);
@@ -224,6 +244,8 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("[Zurako] Log Horizon - 01 - The Apocalypse (BD 1080p AAC) [7AE12174].mkv", false)]
[TestCase("WEEDS.S03E01-06.DUAL.1080p.Blu-ray.AC3.-HELLYWOOD.avi", false)]
[TestCase("[Coalgirls]_Durarara!!_01_(1920x1080_Blu-ray_FLAC)_[8370CB8F].mkv", false)]
[TestCase("Planet.Earth.S01E11.Ocean.Deep.1080p.HD-DVD.DD.VC1-TRB", false)]
[TestCase("Spirited Away(2001) Bluray FHD Hi10P.mkv", false)]
public void should_parse_bluray1080p_quality(string title, bool proper)
{
ParseAndVerifyQuality(title, Quality.Bluray1080p, proper);
@@ -231,6 +253,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("House.of.Cards.US.s05e13.4K.UHD.Bluray", false)]
[TestCase("House.of.Cards.US.s05e13.UHD.4K.Bluray", false)]
[TestCase("[DameDesuYo] Backlog Bundle - Part 1 (BD 4K 8bit FLAC)", false)]
public void should_parse_bluray2160p_quality(string title, bool proper)
{
ParseAndVerifyQuality(title, Quality.Bluray2160p, proper);

View File

@@ -1,4 +1,4 @@
using FluentAssertions;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Tv;
@@ -10,6 +10,7 @@ namespace NzbDrone.Core.Test.TvTests
[TestCase("A to Z", 281588, "a to z")]
[TestCase("A.D. The Bible Continues", 289260, "ad bible continues")]
[TestCase("A.P. Bio", 328534, "ap bio")]
[TestCase("The A-Team", 77904, "ateam")]
public void should_use_precomputed_title(string title, int tvdbId, string expected)
{
SeriesTitleNormalizer.Normalize(title, tvdbId).Should().Be(expected);

View File

@@ -1,7 +1,10 @@
using System;
using System.Data.SQLite;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation;
namespace NzbDrone.Core.Datastore
{
@@ -14,10 +17,20 @@ namespace NzbDrone.Core.Datastore
public class ConnectionStringFactory : IConnectionStringFactory
{
public ConnectionStringFactory(IAppFolderInfo appFolderInfo)
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(ConnectionStringFactory));
public ConnectionStringFactory(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider)
{
MainDbConnectionString = GetConnectionString(appFolderInfo.GetNzbDroneDatabase());
LogDbConnectionString = GetConnectionString(appFolderInfo.GetLogDatabase());
var mount = diskProvider.GetMount(appFolderInfo.AppDataFolder);
var isNetworkDrive = mount.DriveType == System.IO.DriveType.Network;
if (isNetworkDrive)
{
Logger.Warn("AppData folder {0} is located on the network drive {1} using a {2} filesystem. Is highly discouraged to use a SQLite database on network drives and may lead to database corruption.",
appFolderInfo.AppDataFolder, mount.RootDirectory, mount.DriveFormat);
}
MainDbConnectionString = GetConnectionString(appFolderInfo.GetNzbDroneDatabase(), isNetworkDrive);
LogDbConnectionString = GetConnectionString(appFolderInfo.GetLogDatabase(), isNetworkDrive);
}
public string MainDbConnectionString { get; private set; }
@@ -30,14 +43,14 @@ namespace NzbDrone.Core.Datastore
return connectionBuilder.DataSource;
}
private static string GetConnectionString(string dbPath)
private static string GetConnectionString(string dbPath, bool isNetworkDrive)
{
var connectionBuilder = new SQLiteConnectionStringBuilder();
connectionBuilder.DataSource = dbPath;
connectionBuilder.CacheSize = (int)-10.Megabytes();
connectionBuilder.DateTimeKind = DateTimeKind.Utc;
connectionBuilder.JournalMode = OsInfo.IsOsx ? SQLiteJournalModeEnum.Truncate : SQLiteJournalModeEnum.Wal;
connectionBuilder.JournalMode = OsInfo.IsOsx || isNetworkDrive ? SQLiteJournalModeEnum.Truncate : SQLiteJournalModeEnum.Wal;
connectionBuilder.Pooling = true;
connectionBuilder.Version = 3;

View File

@@ -0,0 +1,100 @@
using System;
using System.Linq;
using NLog;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.History;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.DecisionEngine.Specifications
{
public class AlreadyImportedSpecification : IDecisionEngineSpecification
{
private readonly IHistoryService _historyService;
private readonly IConfigService _configService;
private readonly Logger _logger;
public AlreadyImportedSpecification(IHistoryService historyService,
IConfigService configService,
Logger logger)
{
_historyService = historyService;
_configService = configService;
_logger = logger;
}
public SpecificationPriority Priority => SpecificationPriority.Database;
public RejectionType Type => RejectionType.Permanent;
public Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria)
{
var cdhEnabled = _configService.EnableCompletedDownloadHandling;
if (!cdhEnabled)
{
_logger.Debug("Skipping already imported check because CDH is disabled");
return Decision.Accept();
}
_logger.Debug("Performing alerady imported check on report");
foreach (var episode in subject.Episodes)
{
if (!episode.HasFile)
{
_logger.Debug("Skipping already imported check for episode without file");
continue;
}
var historyForEpisode = _historyService.FindByEpisodeId(episode.Id);
var lastGrabbed = historyForEpisode.FirstOrDefault(h => h.EventType == HistoryEventType.Grabbed);
if (lastGrabbed == null)
{
continue;
}
var imported = historyForEpisode.FirstOrDefault(h =>
h.EventType == HistoryEventType.DownloadFolderImported &&
h.DownloadId == lastGrabbed.DownloadId);
if (imported == null)
{
continue;
}
// This is really only a guard against redownloading the same release over
// and over when the grabbed and imported qualities do not match, if they do
// match skip this check.
if (lastGrabbed.Quality.Equals(imported.Quality))
{
continue;
}
var release = subject.Release;
if (release.DownloadProtocol == DownloadProtocol.Torrent)
{
var torrentInfo = release as TorrentInfo;
if (torrentInfo != null && torrentInfo.InfoHash.ToUpper() == lastGrabbed.DownloadId)
{
_logger.Debug("Has same torrent hash as a grabbed and imported release");
return Decision.Reject("Has same torrent hash as a grabbed and imported release");
}
}
// Only based on title because a release with the same title on another indexer/released at
// a different time very likely has the exact same content and we don't need to also try it.
if (release.Title.Equals(lastGrabbed.SourceTitle, StringComparison.InvariantCultureIgnoreCase))
{
_logger.Debug("Has same release name as a grabbed and imported release");
return Decision.Reject("Has same release name as a grabbed and imported release");
}
}
return Decision.Accept();
}
}
}

View File

@@ -30,7 +30,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
if (!subject.Series.Monitored)
{
_logger.Debug("{0} is present in the DB but not tracked. skipping.", subject.Series);
_logger.Debug("{0} is present in the DB but not tracked. Rejecting", subject.Series);
return Decision.Reject("Series is not monitored");
}
@@ -40,8 +40,22 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
return Decision.Accept();
}
_logger.Debug("Only {0}/{1} episodes are monitored. skipping.", monitoredCount, subject.Episodes.Count);
return Decision.Reject("Episode is not monitored");
if (subject.Episodes.Count == 1)
{
_logger.Debug("Episode is not monitored. Rejecting", monitoredCount, subject.Episodes.Count);
return Decision.Reject("Episode is not monitored");
}
if (monitoredCount == 0)
{
_logger.Debug("No episodes in the release are monitored. Rejecting", monitoredCount, subject.Episodes.Count);
}
else
{
_logger.Debug("Only {0}/{1} episodes in the release are monitored. Rejecting", monitoredCount, subject.Episodes.Count);
}
return Decision.Reject("One or more episodes is not monitored");
}
}
}

View File

@@ -23,7 +23,7 @@ namespace NzbDrone.Core.DiskSpace
private readonly IDiskProvider _diskProvider;
private readonly Logger _logger;
private static readonly Regex _regexSpecialDrive = new Regex("^/var/lib/(docker|rancher|kubelet)(/|$)|^/boot(/|$)|/docker(/var)?/aufs(/|$)", RegexOptions.Compiled);
private static readonly Regex _regexSpecialDrive = new Regex("^/var/lib/(docker|rancher|kubelet)(/|$)|^/(boot|etc)(/|$)|/docker(/var)?/aufs(/|$)", RegexOptions.Compiled);
public DiskSpaceService(ISeriesService seriesService, IConfigService configService, IDiskProvider diskProvider, Logger logger)
{

View File

@@ -104,6 +104,8 @@ namespace NzbDrone.Core.Download.Clients.Deluge
foreach (var torrent in torrents)
{
if (torrent.Hash == null) continue;
var item = new DownloadClientItem();
item.DownloadId = torrent.Hash.ToUpper();
item.Title = torrent.Name;

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -13,6 +13,7 @@ using NzbDrone.Core.Download.Clients.DownloadStation.Proxies;
using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Download.Clients.DownloadStation
@@ -47,6 +48,8 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
public override string Name => "Download Station";
public override ProviderMessage Message => new ProviderMessage("Sonarr is unable to connect to Download Station if 2-Factor Authentication is enabled on your DSM account", ProviderMessageType.Warning);
protected IEnumerable<DownloadStationTask> GetTasks()
{
return _dsTaskProxy.GetTasks(Settings).Where(v => v.Type.ToLower() == DownloadStationTaskType.BT.ToString().ToLower());

View File

@@ -11,6 +11,7 @@ using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download.Clients.DownloadStation.Proxies;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Download.Clients.DownloadStation
@@ -46,6 +47,8 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
public override string Name => "Download Station";
public override ProviderMessage Message => new ProviderMessage("Sonarr is unable to connect to Download Station if 2-Factor Authentication is enabled on your DSM account", ProviderMessageType.Warning);
protected IEnumerable<DownloadStationTask> GetTasks()
{
return _dsTaskProxy.GetTasks(Settings).Where(v => v.Type.ToLower() == DownloadStationTaskType.NZB.ToString().ToLower());

View File

@@ -286,7 +286,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
{
return new NzbDroneValidationFailure("TvCategory", "Category does not exist")
{
InfoLink = string.Format("http://{0}:{1}/", Settings.Host, Settings.Port),
InfoLink = _proxy.GetBaseUrl(Settings),
DetailedDescription = "The Category your entered doesn't exist in NzbGet. Go to NzbGet to create it."
};
}
@@ -304,7 +304,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
{
return new NzbDroneValidationFailure(string.Empty, "NzbGet setting KeepHistory should be greater than 0")
{
InfoLink = string.Format("http://{0}:{1}/", Settings.Host, Settings.Port),
InfoLink = _proxy.GetBaseUrl(Settings),
DetailedDescription = "NzbGet setting KeepHistory is set to 0. Which prevents Sonarr from seeing completed downloads."
};
}
@@ -312,7 +312,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
{
return new NzbDroneValidationFailure(string.Empty, "NzbGet setting KeepHistory should be less than 25000")
{
InfoLink = string.Format("http://{0}:{1}/", Settings.Host, Settings.Port),
InfoLink = _proxy.GetBaseUrl(Settings),
DetailedDescription = "NzbGet setting KeepHistory is set too high."
};
}

View File

@@ -11,6 +11,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
{
public interface INzbgetProxy
{
string GetBaseUrl(NzbgetSettings settings, string relativePath = null);
string DownloadNzb(byte[] nzbData, string title, string category, int priority, bool addpaused, NzbgetSettings settings);
NzbgetGlobalStatus GetGlobalStatus(NzbgetSettings settings);
List<NzbgetQueueItem> GetQueue(NzbgetSettings settings);
@@ -36,9 +37,17 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
_versionCache = cacheManager.GetCache<string>(GetType(), "versions");
}
public string GetBaseUrl(NzbgetSettings settings, string relativePath = null)
{
var baseUrl = HttpRequestBuilder.BuildBaseUrl(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase);
baseUrl = HttpUri.CombinePath(baseUrl, relativePath);
return baseUrl;
}
private bool HasVersion(int minimumVersion, NzbgetSettings settings)
{
var versionString = _versionCache.Find(settings.Host + ":" + settings.Port) ?? GetVersion(settings);
var versionString = _versionCache.Find(GetBaseUrl(settings)) ?? GetVersion(settings);
var version = int.Parse(versionString.Split(new[] { '.', '-' })[0]);
@@ -139,7 +148,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
{
var response = ProcessRequest<string>(settings, "version");
_versionCache.Set(settings.Host + ":" + settings.Port, response, TimeSpan.FromDays(1));
_versionCache.Set(GetBaseUrl(settings), response, TimeSpan.FromDays(1));
return response;
}
@@ -170,7 +179,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
queueItem = queue.SingleOrDefault(h => h.Parameters.Any(p => p.Name == "drone" && id == (p.Value as string)));
historyItem = history.SingleOrDefault(h => h.Parameters.Any(p => p.Name == "drone" && id == (p.Value as string)));
}
if (queueItem != null)
{
if (!EditQueue("GroupFinalDelete", 0, "", queueItem.NzbId, settings))
@@ -218,7 +227,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
private T ProcessRequest<T>(NzbgetSettings settings, string method, params object[] parameters)
{
var baseUrl = HttpRequestBuilder.BuildBaseUrl(settings.UseSsl, settings.Host, settings.Port, "jsonrpc");
var baseUrl = GetBaseUrl(settings, "jsonrpc");
var requestBuilder = new JsonRpcRequestBuilder(baseUrl, method, parameters);
requestBuilder.LogResponseContent = true;

View File

@@ -1,4 +1,5 @@
using FluentValidation;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
@@ -11,6 +12,8 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
{
RuleFor(c => c.Host).ValidHost();
RuleFor(c => c.Port).InclusiveBetween(1, 65535);
RuleFor(c => c.UrlBase).ValidUrlBase().When(c => c.UrlBase.IsNotNullOrWhiteSpace());
RuleFor(c => c.Username).NotEmpty().When(c => !string.IsNullOrWhiteSpace(c.Password));
RuleFor(c => c.Password).NotEmpty().When(c => !string.IsNullOrWhiteSpace(c.Username));
@@ -37,25 +40,28 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
[FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)]
public int Port { get; set; }
[FieldDefinition(2, Label = "Username", Type = FieldType.Textbox)]
[FieldDefinition(2, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the nzbget url, e.g. http://[host]:[port]/[urlBase]/jsonrpc")]
public string UrlBase { get; set; }
[FieldDefinition(3, Label = "Username", Type = FieldType.Textbox)]
public string Username { get; set; }
[FieldDefinition(3, Label = "Password", Type = FieldType.Password)]
[FieldDefinition(4, Label = "Password", Type = FieldType.Password)]
public string Password { get; set; }
[FieldDefinition(4, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated downloads, but it's optional")]
[FieldDefinition(5, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated downloads, but it's optional")]
public string TvCategory { get; set; }
[FieldDefinition(5, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(NzbgetPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")]
[FieldDefinition(6, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(NzbgetPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")]
public int RecentTvPriority { get; set; }
[FieldDefinition(6, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(NzbgetPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")]
[FieldDefinition(7, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(NzbgetPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")]
public int OlderTvPriority { get; set; }
[FieldDefinition(7, Label = "Add Paused", Type = FieldType.Checkbox, HelpText = "This option requires at least NzbGet version 16.0")]
[FieldDefinition(8, Label = "Add Paused", Type = FieldType.Checkbox, HelpText = "This option requires at least NzbGet version 16.0")]
public bool AddPaused { get; set; }
[FieldDefinition(8, Label = "Use SSL", Type = FieldType.Checkbox)]
[FieldDefinition(9, Label = "Use SSL", Type = FieldType.Checkbox)]
public bool UseSsl { get; set; }
public NzbDroneValidationResult Validate()

View File

@@ -142,6 +142,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
case "stalledUP": // torrent is being seeded, but no connection were made
case "queuedUP": // queuing is enabled and torrent is queued for upload
case "checkingUP": // torrent has finished downloading and is being checked
case "forcedUP": // torrent has finished downloading and is being forcibly seeded
item.Status = DownloadItemStatus.Completed;
item.RemainingTime = TimeSpan.Zero; // qBittorrent sends eta=8640000 for completed torrents
break;

View File

@@ -419,7 +419,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
{
return new NzbDroneValidationFailure("", "Disable 'Check before download' option in Sabnbzd")
{
InfoLink = string.Format("http://{0}:{1}/sabnzbd/config/switches/", Settings.Host, Settings.Port),
InfoLink = _proxy.GetBaseUrl(Settings, "config/switches/"),
DetailedDescription = "Using Check before download affects Sonarr ability to track new downloads. Also Sabnzbd recommends 'Abort jobs that cannot be completed' instead since it's more effective."
};
}
@@ -438,7 +438,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
{
return new NzbDroneValidationFailure("TvCategory", "Enable Job folders")
{
InfoLink = string.Format("http://{0}:{1}/sabnzbd/config/categories/", Settings.Host, Settings.Port),
InfoLink = _proxy.GetBaseUrl(Settings, "config/categories/"),
DetailedDescription = "Sonarr prefers each download to have a separate folder. With * appended to the Folder/Path Sabnzbd will not create these job folders. Go to Sabnzbd to fix it."
};
}
@@ -449,7 +449,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
{
return new NzbDroneValidationFailure("TvCategory", "Category does not exist")
{
InfoLink = string.Format("http://{0}:{1}/sabnzbd/config/categories/", Settings.Host, Settings.Port),
InfoLink = _proxy.GetBaseUrl(Settings, "config/categories/"),
DetailedDescription = "The Category your entered doesn't exist in Sabnzbd. Go to Sabnzbd to create it."
};
}
@@ -458,7 +458,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
{
return new NzbDroneValidationFailure("TvCategory", "Disable TV Sorting")
{
InfoLink = string.Format("http://{0}:{1}/sabnzbd/config/sorting/", Settings.Host, Settings.Port),
InfoLink = _proxy.GetBaseUrl(Settings, "config/sorting/"),
DetailedDescription = "You must disable Sabnzbd TV Sorting for the category Sonarr uses to prevent import issues. Go to Sabnzbd to fix it."
};
}
@@ -466,7 +466,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
{
return new NzbDroneValidationFailure("TvCategory", "Disable Movie Sorting")
{
InfoLink = string.Format("http://{0}:{1}/sabnzbd/config/sorting/", Settings.Host, Settings.Port),
InfoLink = _proxy.GetBaseUrl(Settings, "config/sorting/"),
DetailedDescription = "You must disable Sabnzbd Movie Sorting for the category Sonarr uses to prevent import issues. Go to Sabnzbd to fix it."
};
}
@@ -474,7 +474,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
{
return new NzbDroneValidationFailure("TvCategory", "Disable Date Sorting")
{
InfoLink = string.Format("http://{0}:{1}/sabnzbd/config/sorting/", Settings.Host, Settings.Port),
InfoLink = _proxy.GetBaseUrl(Settings, "config/sorting/"),
DetailedDescription = "You must disable Sabnzbd Date Sorting for the category Sonarr uses to prevent import issues. Go to Sabnzbd to fix it."
};
}

View File

@@ -11,6 +11,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
{
public interface ISabnzbdProxy
{
string GetBaseUrl(SabnzbdSettings settings, string relativePath = null);
SabnzbdAddResponse DownloadNzb(byte[] nzbData, string filename, string category, int priority, SabnzbdSettings settings);
void RemoveFrom(string source, string id,bool deleteData, SabnzbdSettings settings);
string GetVersion(SabnzbdSettings settings);
@@ -32,6 +33,14 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
_logger = logger;
}
public string GetBaseUrl(SabnzbdSettings settings, string relativePath = null)
{
var baseUrl = HttpRequestBuilder.BuildBaseUrl(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase);
baseUrl = HttpUri.CombinePath(baseUrl, relativePath);
return baseUrl;
}
public SabnzbdAddResponse DownloadNzb(byte[] nzbData, string filename, string category, int priority, SabnzbdSettings settings)
{
var request = BuildRequest("addfile", settings).Post();
@@ -140,10 +149,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
private HttpRequestBuilder BuildRequest(string mode, SabnzbdSettings settings)
{
var baseUrl = string.Format(@"{0}://{1}:{2}/api",
settings.UseSsl ? "https" : "http",
settings.Host,
settings.Port);
var baseUrl = GetBaseUrl(settings, "api");
var requestBuilder = new HttpRequestBuilder(baseUrl)
.Accept(HttpAccept.Json)

View File

@@ -1,4 +1,5 @@
using FluentValidation;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
@@ -11,6 +12,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
{
RuleFor(c => c.Host).ValidHost();
RuleFor(c => c.Port).InclusiveBetween(1, 65535);
RuleFor(c => c.UrlBase).ValidUrlBase().When(c => c.UrlBase.IsNotNullOrWhiteSpace());
RuleFor(c => c.ApiKey).NotEmpty()
.WithMessage("API Key is required when username/password are not configured")
@@ -49,25 +51,28 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
[FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)]
public int Port { get; set; }
[FieldDefinition(2, Label = "API Key", Type = FieldType.Textbox)]
[FieldDefinition(2, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the Sabnzbd url, e.g. http://[host]:[port]/[urlBase]/api")]
public string UrlBase { get; set; }
[FieldDefinition(3, Label = "API Key", Type = FieldType.Textbox)]
public string ApiKey { get; set; }
[FieldDefinition(3, Label = "Username", Type = FieldType.Textbox)]
[FieldDefinition(4, Label = "Username", Type = FieldType.Textbox)]
public string Username { get; set; }
[FieldDefinition(4, Label = "Password", Type = FieldType.Password)]
[FieldDefinition(5, Label = "Password", Type = FieldType.Password)]
public string Password { get; set; }
[FieldDefinition(5, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated downloads, but it's optional")]
[FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated downloads, but it's optional")]
public string TvCategory { get; set; }
[FieldDefinition(6, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(SabnzbdPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")]
[FieldDefinition(7, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(SabnzbdPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")]
public int RecentTvPriority { get; set; }
[FieldDefinition(7, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(SabnzbdPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")]
[FieldDefinition(8, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(SabnzbdPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")]
public int OlderTvPriority { get; set; }
[FieldDefinition(8, Label = "Use SSL", Type = FieldType.Checkbox)]
[FieldDefinition(9, Label = "Use SSL", Type = FieldType.Checkbox)]
public bool UseSsl { get; set; }
public NzbDroneValidationResult Validate()

View File

@@ -172,7 +172,8 @@ namespace NzbDrone.Core.Download.Clients.Transmission
"errorString",
"uploadedEver",
"downloadedEver",
"seedRatioLimit"
"seedRatioLimit",
"fileCount"
};
var arguments = new Dictionary<string, object>();

View File

@@ -3,31 +3,19 @@
public class TransmissionTorrent
{
public int Id { get; set; }
public string HashString { get; set; }
public string Name { get; set; }
public string DownloadDir { get; set; }
public long TotalSize { get; set; }
public long LeftUntilDone { get; set; }
public bool IsFinished { get; set; }
public int Eta { get; set; }
public TransmissionTorrentStatus Status { get; set; }
public int SecondsDownloading { get; set; }
public string ErrorString { get; set; }
public long DownloadedEver { get; set; }
public long UploadedEver { get; set; }
public long SeedRatioLimit { get; set; }
public int FileCount { get; set; }
}
}

View File

@@ -30,7 +30,7 @@ namespace NzbDrone.Core.Download.Clients.Vuze
// - A multi-file torrent is downloaded in a job folder and 'outputPath' points to that directory directly.
// - A single-file torrent is downloaded in the root folder and 'outputPath' poinst to that root folder.
// We have to make sure the return value points to the job folder OR file.
if (outputPath == null || outputPath.FileName == torrent.Name)
if (outputPath == null || outputPath.FileName == torrent.Name || torrent.FileCount > 1)
{
_logger.Trace("Vuze output directory: {0}", outputPath);
}

View File

@@ -2,7 +2,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
{
public enum RTorrentPriority
{
DoNotDownload = 0,
VeryLow = 0,
Low = 1,
Normal = 2,
High = 3

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
@@ -26,9 +26,15 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
[XmlRpcMethod("d.multicall2")]
object[] TorrentMulticall(params string[] parameters);
[XmlRpcMethod("load.normal")]
int LoadNormal(string target, string data, params string[] commands);
[XmlRpcMethod("load.start")]
int LoadStart(string target, string data, params string[] commands);
[XmlRpcMethod("load.raw")]
int LoadRaw(string target, byte[] data, params string[] commands);
[XmlRpcMethod("load.raw_start")]
int LoadRawStart(string target, byte[] data, params string[] commands);
@@ -107,10 +113,20 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
public void AddTorrentFromUrl(string torrentUrl, string label, RTorrentPriority priority, string directory, RTorrentSettings settings)
{
_logger.Debug("Executing remote method: load.normal");
var client = BuildClient(settings);
var response = ExecuteRequest(() => client.LoadStart("", torrentUrl, GetCommands(label, priority, directory)));
var response = ExecuteRequest(() =>
{
if (settings.AddStopped)
{
_logger.Debug("Executing remote method: load.normal");
return client.LoadNormal("", torrentUrl, GetCommands(label, priority, directory));
}
else
{
_logger.Debug("Executing remote method: load.start");
return client.LoadStart("", torrentUrl, GetCommands(label, priority, directory));
}
});
if (response != 0)
{
@@ -120,10 +136,20 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
public void AddTorrentFromFile(string fileName, byte[] fileContent, string label, RTorrentPriority priority, string directory, RTorrentSettings settings)
{
_logger.Debug("Executing remote method: load.raw");
var client = BuildClient(settings);
var response = ExecuteRequest(() => client.LoadRawStart("", fileContent, GetCommands(label, priority, directory)));
var response = ExecuteRequest(() =>
{
if (settings.AddStopped)
{
_logger.Debug("Executing remote method: load.raw");
return client.LoadRaw("", fileContent, GetCommands(label, priority, directory));
}
else
{
_logger.Debug("Executing remote method: load.raw_start");
return client.LoadRawStart("", fileContent, GetCommands(label, priority, directory));
}
});
if (response != 0)
{

View File

@@ -1,4 +1,4 @@
using FluentValidation;
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
@@ -61,6 +61,9 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
[FieldDefinition(9, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(RTorrentPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")]
public int OlderTvPriority { get; set; }
[FieldDefinition(10, Label = "Add Stopped", Type = FieldType.Checkbox, HelpText = "Enabling will prevent magnets from downloading before downloading")]
public bool AddStopped { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -163,6 +163,18 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
tvShow.Add(new XElement("mpaa", series.Certification));
tvShow.Add(new XElement("id", series.TvdbId));
var uniqueId = new XElement("uniqueid", series.TvdbId);
uniqueId.SetAttributeValue("type", "tvdb");
uniqueId.SetAttributeValue("default", true);
tvShow.Add(uniqueId);
if (series.ImdbId.IsNotNullOrWhiteSpace())
{
var imdbId = new XElement("uniqueid", series.ImdbId);
imdbId.SetAttributeValue("type", "imdb");
tvShow.Add(imdbId);
}
foreach (var genre in series.Genres)
{
tvShow.Add(new XElement("genre", genre));

View File

@@ -16,18 +16,18 @@ namespace NzbDrone.Core.Extras.Metadata
public class ExistingMetadataImporter : ImportExistingExtraFilesBase<MetadataFile>
{
private readonly IExtraFileService<MetadataFile> _metadataFileService;
private readonly IAugmentingService _augmentingService;
private readonly IAggregationService _aggregationService;
private readonly Logger _logger;
private readonly List<IMetadata> _consumers;
public ExistingMetadataImporter(IExtraFileService<MetadataFile> metadataFileService,
IEnumerable<IMetadata> consumers,
IAugmentingService augmentingService,
IAggregationService aggregationService,
Logger logger)
: base(metadataFileService)
{
_metadataFileService = metadataFileService;
_augmentingService = augmentingService;
_aggregationService = aggregationService;
_logger = logger;
_consumers = consumers.ToList();
}
@@ -71,7 +71,7 @@ namespace NzbDrone.Core.Extras.Metadata
try
{
_augmentingService.Augment(localEpisode, false);
_aggregationService.Augment(localEpisode, false);
}
catch (AugmentingFailedException ex)
{

View File

@@ -15,16 +15,16 @@ namespace NzbDrone.Core.Extras.Others
public class ExistingOtherExtraImporter : ImportExistingExtraFilesBase<OtherExtraFile>
{
private readonly IExtraFileService<OtherExtraFile> _otherExtraFileService;
private readonly IAugmentingService _augmentingService;
private readonly IAggregationService _aggregationService;
private readonly Logger _logger;
public ExistingOtherExtraImporter(IExtraFileService<OtherExtraFile> otherExtraFileService,
IAugmentingService augmentingService,
IAggregationService aggregationService,
Logger logger)
: base(otherExtraFileService)
{
_otherExtraFileService = otherExtraFileService;
_augmentingService = augmentingService;
_aggregationService = aggregationService;
_logger = logger;
}
@@ -56,7 +56,7 @@ namespace NzbDrone.Core.Extras.Others
try
{
_augmentingService.Augment(localEpisode, false);
_aggregationService.Augment(localEpisode, false);
}
catch (AugmentingFailedException ex)
{

View File

@@ -14,16 +14,16 @@ namespace NzbDrone.Core.Extras.Subtitles
public class ExistingSubtitleImporter : ImportExistingExtraFilesBase<SubtitleFile>
{
private readonly IExtraFileService<SubtitleFile> _subtitleFileService;
private readonly IAugmentingService _augmentingService;
private readonly IAggregationService _aggregationService;
private readonly Logger _logger;
public ExistingSubtitleImporter(IExtraFileService<SubtitleFile> subtitleFileService,
IAugmentingService augmentingService,
IAggregationService aggregationService,
Logger logger)
: base (subtitleFileService)
{
_subtitleFileService = subtitleFileService;
_augmentingService = augmentingService;
_aggregationService = aggregationService;
_logger = logger;
}
@@ -51,7 +51,7 @@ namespace NzbDrone.Core.Extras.Subtitles
try
{
_augmentingService.Augment(localEpisode, false);
_aggregationService.Augment(localEpisode, false);
}
catch (AugmentingFailedException ex)
{

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
namespace NzbDrone.Core.Extras.Subtitles
@@ -24,7 +24,8 @@ namespace NzbDrone.Core.Extras.Subtitles
".txt",
".utf",
".utf8",
".utf-8"
".utf-8",
".vtt"
};
}

View File

@@ -13,6 +13,7 @@ namespace NzbDrone.Core.History
{
List<QualityModel> GetBestQualityInHistory(int episodeId);
History MostRecentForEpisode(int episodeId);
List<History> FindByEpisodeId(int episodeId);
History MostRecentForDownloadId(string downloadId);
List<History> FindByDownloadId(string downloadId);
List<History> FindDownloadHistory(int idSeriesId, QualityModel quality);
@@ -43,6 +44,13 @@ namespace NzbDrone.Core.History
.FirstOrDefault();
}
public List<History> FindByEpisodeId(int episodeId)
{
return Query.Where(h => h.EpisodeId == episodeId)
.OrderByDescending(h => h.Date)
.ToList();
}
public History MostRecentForDownloadId(string downloadId)
{
return Query.Where(h => h.DownloadId == downloadId)

View File

@@ -21,6 +21,7 @@ namespace NzbDrone.Core.History
QualityModel GetBestQualityInHistory(Profile profile, int episodeId);
PagingSpec<History> Paged(PagingSpec<History> pagingSpec);
History MostRecentForEpisode(int episodeId);
List<History> FindByEpisodeId(int episodeId);
History MostRecentForDownloadId(string downloadId);
History Get(int historyId);
List<History> Find(string downloadId, HistoryEventType eventType);
@@ -55,6 +56,11 @@ namespace NzbDrone.Core.History
return _historyRepository.MostRecentForEpisode(episodeId);
}
public List<History> FindByEpisodeId(int episodeId)
{
return _historyRepository.FindByEpisodeId(episodeId);
}
public History MostRecentForDownloadId(string downloadId)
{
return _historyRepository.MostRecentForDownloadId(downloadId);

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@@ -253,12 +253,25 @@ namespace NzbDrone.Core.Indexers
protected virtual RssEnclosure[] GetEnclosures(XElement item)
{
var enclosures = item.Elements("enclosure")
.Select(v => new RssEnclosure
.Select(v =>
{
Url = v.Attribute("url").Value,
Type = v.Attribute("type").Value,
Length = (long)v.Attribute("length")
try
{
return new RssEnclosure
{
Url = v.Attribute("url").Value,
Type = v.Attribute("type").Value,
Length = (long)v.Attribute("length")
};
}
catch (Exception e)
{
_logger.Warn(e, "Failed to get enclosure for: {0}", item.Title());
}
return null;
})
.Where(v => v != null)
.ToArray();
return enclosures;

View File

@@ -10,12 +10,12 @@ using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation
{
public interface IAugmentingService
public interface IAggregationService
{
LocalEpisode Augment(LocalEpisode localEpisode, bool otherFiles);
}
public class AugmentingService : IAugmentingService
public class AggregationService : IAggregationService
{
private readonly IEnumerable<IAggregateLocalEpisode> _augmenters;
private readonly IDiskProvider _diskProvider;
@@ -23,7 +23,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation
private readonly IConfigService _configService;
private readonly Logger _logger;
public AugmentingService(IEnumerable<IAggregateLocalEpisode> augmenters,
public AggregationService(IEnumerable<IAggregateLocalEpisode> augmenters,
IDiskProvider diskProvider,
IVideoFileInfoReader videoFileInfoReader,
IConfigService configService,
@@ -38,11 +38,13 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation
public LocalEpisode Augment(LocalEpisode localEpisode, bool otherFiles)
{
var isMediaFile = MediaFileExtensions.Extensions.Contains(Path.GetExtension(localEpisode.Path));
if (localEpisode.DownloadClientEpisodeInfo == null &&
localEpisode.FolderEpisodeInfo == null &&
localEpisode.FileEpisodeInfo == null)
{
if (MediaFileExtensions.Extensions.Contains(Path.GetExtension(localEpisode.Path)))
if (isMediaFile)
{
throw new AugmentingFailedException("Unable to parse episode info from path: {0}", localEpisode.Path);
}
@@ -50,7 +52,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation
localEpisode.Size = _diskProvider.GetFileSize(localEpisode.Path);
if (!localEpisode.ExistingFile || _configService.EnableMediaInfo)
if (isMediaFile && (!localEpisode.ExistingFile || _configService.EnableMediaInfo))
{
localEpisode.MediaInfo = _videoFileInfoReader.GetMediaInfo(localEpisode.Path);
}

View File

@@ -30,11 +30,15 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators
if (!otherFiles && !SceneChecker.IsSceneTitle(Path.GetFileNameWithoutExtension(localEpisode.Path)))
{
if (downloadClientEpisodeInfo != null && !downloadClientEpisodeInfo.FullSeason)
if (downloadClientEpisodeInfo != null &&
!downloadClientEpisodeInfo.FullSeason &&
PreferOtherEpisodeInfo(parsedEpisodeInfo, downloadClientEpisodeInfo))
{
parsedEpisodeInfo = localEpisode.DownloadClientEpisodeInfo;
}
else if (folderEpisodeInfo != null && !folderEpisodeInfo.FullSeason)
else if (folderEpisodeInfo != null &&
!folderEpisodeInfo.FullSeason &&
PreferOtherEpisodeInfo(parsedEpisodeInfo, folderEpisodeInfo))
{
parsedEpisodeInfo = localEpisode.FolderEpisodeInfo;
}
@@ -45,7 +49,10 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators
var title = Path.GetFileNameWithoutExtension(localEpisode.Path);
var specialEpisodeInfo = _parsingService.ParseSpecialEpisodeTitle(parsedEpisodeInfo, title, localEpisode.Series);
return specialEpisodeInfo;
if (specialEpisodeInfo != null)
{
parsedEpisodeInfo = specialEpisodeInfo;
}
}
return parsedEpisodeInfo;
@@ -68,5 +75,21 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators
return new List<Episode>();
}
private bool PreferOtherEpisodeInfo(ParsedEpisodeInfo fileEpisodeInfo, ParsedEpisodeInfo otherEpisodeInfo)
{
if (fileEpisodeInfo == null)
{
return true;
}
// When the files episode info is not absolute prefer it over a parsed episode info that is absolute
if (!fileEpisodeInfo.IsAbsoluteNumbering && otherEpisodeInfo.IsAbsoluteNumbering)
{
return false;
}
return true;
}
}
}

View File

@@ -23,21 +23,21 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
{
private readonly IEnumerable<IImportDecisionEngineSpecification> _specifications;
private readonly IMediaFileService _mediaFileService;
private readonly IAugmentingService _augmentingService;
private readonly IAggregationService _aggregationService;
private readonly IDiskProvider _diskProvider;
private readonly IDetectSample _detectSample;
private readonly Logger _logger;
public ImportDecisionMaker(IEnumerable<IImportDecisionEngineSpecification> specifications,
IMediaFileService mediaFileService,
IAugmentingService augmentingService,
IAggregationService aggregationService,
IDiskProvider diskProvider,
IDetectSample detectSample,
Logger logger)
{
_specifications = specifications;
_mediaFileService = mediaFileService;
_augmentingService = augmentingService;
_aggregationService = aggregationService;
_diskProvider = diskProvider;
_detectSample = detectSample;
_logger = logger;
@@ -61,7 +61,9 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
downloadClientItemInfo = Parser.Parser.ParseTitle(downloadClientItem.Title);
}
var nonSampleVideoFileCount = GetNonSampleVideoFileCount(newFiles, series, downloadClientItemInfo, folderInfo);
// If not importing from a scene source (series folder for example), then assume all files are not samples
// to avoid using media info on every file needlessly (especially if Analyse Media Files is disabled).
var nonSampleVideoFileCount = sceneSource ? GetNonSampleVideoFileCount(newFiles, series, downloadClientItemInfo, folderInfo) : videoFiles.Count;
var decisions = new List<ImportDecision>();
@@ -94,7 +96,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
try
{
_augmentingService.Augment(localEpisode, otherFiles);
_aggregationService.Augment(localEpisode, otherFiles);
if (localEpisode.Episodes.Empty())
{

View File

@@ -1,14 +1,47 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Qualities;
namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
{
public class ManualImportFile
public class ManualImportFile : IEquatable<ManualImportFile>
{
public string Path { get; set; }
public string FolderName { get; set; }
public int SeriesId { get; set; }
public List<int> EpisodeIds { get; set; }
public QualityModel Quality { get; set; }
public string DownloadId { get; set; }
public bool Equals(ManualImportFile other)
{
if (other == null)
{
return false;
}
return Path.PathEquals(other.Path);
}
public override bool Equals(object obj)
{
if (obj == null)
{
return false;
}
if (obj.GetType() != GetType())
{
return false;
}
return Path.PathEquals(((ManualImportFile)obj).Path);
}
public override int GetHashCode()
{
return Path != null ? Path.GetHashCode() : 0;
}
}
}

View File

@@ -9,6 +9,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
{
public string Path { get; set; }
public string RelativePath { get; set; }
public string FolderName { get; set; }
public string Name { get; set; }
public long Size { get; set; }
public Series Series { get; set; }

View File

@@ -34,7 +34,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
private readonly IEpisodeService _episodeService;
private readonly IVideoFileInfoReader _videoFileInfoReader;
private readonly IImportApprovedEpisodes _importApprovedEpisodes;
private readonly IAugmentingService _augmentingService;
private readonly IAggregationService _aggregationService;
private readonly ITrackedDownloadService _trackedDownloadService;
private readonly IDownloadedEpisodesImportService _downloadedEpisodesImportService;
private readonly IEventAggregator _eventAggregator;
@@ -47,7 +47,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
ISeriesService seriesService,
IEpisodeService episodeService,
IVideoFileInfoReader videoFileInfoReader,
IAugmentingService augmentingService,
IAggregationService aggregationService,
IImportApprovedEpisodes importApprovedEpisodes,
ITrackedDownloadService trackedDownloadService,
IDownloadedEpisodesImportService downloadedEpisodesImportService,
@@ -61,7 +61,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
_seriesService = seriesService;
_episodeService = episodeService;
_videoFileInfoReader = videoFileInfoReader;
_augmentingService = augmentingService;
_aggregationService = aggregationService;
_importApprovedEpisodes = importApprovedEpisodes;
_trackedDownloadService = trackedDownloadService;
_downloadedEpisodesImportService = downloadedEpisodesImportService;
@@ -90,16 +90,17 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
return new List<ManualImportItem>();
}
return new List<ManualImportItem> { ProcessFile(path, downloadId) };
var rootFolder = Path.GetDirectoryName(path);
return new List<ManualImportItem> { ProcessFile(rootFolder, rootFolder, path, downloadId) };
}
return ProcessFolder(path, downloadId);
return ProcessFolder(path, path, downloadId);
}
private List<ManualImportItem> ProcessFolder(string folder, string downloadId)
private List<ManualImportItem> ProcessFolder(string rootFolder, string baseFolder, string downloadId)
{
DownloadClientItem downloadClientItem = null;
var directoryInfo = new DirectoryInfo(folder);
var directoryInfo = new DirectoryInfo(baseFolder);
var series = _parsingService.GetSeries(directoryInfo.Name);
if (downloadId.IsNotNullOrWhiteSpace())
@@ -115,27 +116,26 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
if (series == null)
{
var files = _diskScanService.FilterFiles(folder, _diskScanService.GetVideoFiles(folder));
var files = _diskScanService.FilterFiles(baseFolder, _diskScanService.GetVideoFiles(baseFolder, false));
var subfolders = _diskScanService.FilterFiles(baseFolder, _diskProvider.GetDirectories(baseFolder));
return files.Select(file => ProcessFile(file, downloadId, folder)).Where(i => i != null).ToList();
var processedFiles = files.Select(file => ProcessFile(rootFolder, baseFolder, file, downloadId));
var processedFolders = subfolders.SelectMany(subfolder => ProcessFolder(rootFolder, subfolder, downloadId));
return processedFiles.Concat(processedFolders).Where(i => i != null).ToList();
}
var folderInfo = Parser.Parser.ParseTitle(directoryInfo.Name);
var seriesFiles = _diskScanService.GetVideoFiles(folder).ToList();
var decisions = _importDecisionMaker.GetImportDecisions(seriesFiles, series, downloadClientItem, folderInfo, SceneSource(series, folder));
var seriesFiles = _diskScanService.GetVideoFiles(baseFolder).ToList();
var decisions = _importDecisionMaker.GetImportDecisions(seriesFiles, series, downloadClientItem, folderInfo, SceneSource(series, baseFolder));
return decisions.Select(decision => MapItem(decision, folder, downloadId)).ToList();
return decisions.Select(decision => MapItem(decision, rootFolder, downloadId, directoryInfo.Name)).ToList();
}
private ManualImportItem ProcessFile(string file, string downloadId, string folder = null)
private ManualImportItem ProcessFile(string rootFolder, string baseFolder, string file, string downloadId)
{
if (folder.IsNullOrWhiteSpace())
{
folder = new FileInfo(file).Directory.FullName;
}
DownloadClientItem downloadClientItem = null;
var relativeFile = folder.GetRelativePath(file);
var relativeFile = baseFolder.GetRelativePath(file);
var series = _parsingService.GetSeries(relativeFile.Split('\\', '/')[0]);
if (series == null)
@@ -171,23 +171,25 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
localEpisode.Quality = QualityParser.ParseQuality(file);
localEpisode.Size = _diskProvider.GetFileSize(file);
return MapItem(new ImportDecision(localEpisode, new Rejection("Unknown Series")), folder, downloadId);
return MapItem(new ImportDecision(localEpisode, new Rejection("Unknown Series")), rootFolder, downloadId, null);
}
var importDecisions = _importDecisionMaker.GetImportDecisions(new List<string> {file},
series, downloadClientItem, null, SceneSource(series, folder));
series, downloadClientItem, null, SceneSource(series, baseFolder));
return importDecisions.Any() ? MapItem(importDecisions.First(), folder, downloadId) : new ManualImportItem
{
DownloadId = downloadId,
Path = file,
RelativePath = folder.GetRelativePath(file),
Name = Path.GetFileNameWithoutExtension(file),
Rejections = new List<Rejection>
{
new Rejection("Unable to process file")
}
};
if (importDecisions.Any())
{
return MapItem(importDecisions.First(), rootFolder, downloadId, null);
}
return new ManualImportItem
{
DownloadId = downloadId,
Path = file,
RelativePath = rootFolder.GetRelativePath(file),
Name = Path.GetFileNameWithoutExtension(file),
Rejections = new List<Rejection>()
};
}
private bool SceneSource(Series series, string folder)
@@ -195,12 +197,13 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
return !(series.Path.PathEquals(folder) || series.Path.IsParentPath(folder));
}
private ManualImportItem MapItem(ImportDecision decision, string folder, string downloadId)
private ManualImportItem MapItem(ImportDecision decision, string rootFolder, string downloadId, string folderName)
{
var item = new ManualImportItem();
item.Path = decision.LocalEpisode.Path;
item.RelativePath = folder.GetRelativePath(decision.LocalEpisode.Path);
item.FolderName = folderName;
item.RelativePath = rootFolder.GetRelativePath(decision.LocalEpisode.Path);
item.Name = Path.GetFileNameWithoutExtension(decision.LocalEpisode.Path);
item.DownloadId = downloadId;
@@ -250,14 +253,13 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
var series = _seriesService.GetSeries(file.SeriesId);
var episodes = _episodeService.GetEpisodes(file.EpisodeIds);
var fileEpisodeInfo = Parser.Parser.ParsePath(file.Path) ?? new ParsedEpisodeInfo();
var mediaInfo = _videoFileInfoReader.GetMediaInfo(file.Path);
var existingFile = series.Path.IsParentPath(file.Path);
TrackedDownload trackedDownload = null;
var localEpisode = new LocalEpisode
{
ExistingFile = false,
Episodes = episodes,
MediaInfo = mediaInfo,
FileEpisodeInfo = fileEpisodeInfo,
Path = file.Path,
Quality = file.Quality,
@@ -265,20 +267,37 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
Size = 0
};
//TODO: Cleanup non-tracked downloads
if (file.DownloadId.IsNotNullOrWhiteSpace())
{
trackedDownload = _trackedDownloadService.Find(file.DownloadId);
if (trackedDownload != null)
{
localEpisode.DownloadClientEpisodeInfo = trackedDownload.RemoteEpisode.ParsedEpisodeInfo;
}
}
localEpisode = _augmentingService.Augment(localEpisode, false);
if (file.FolderName.IsNotNullOrWhiteSpace())
{
localEpisode.FolderEpisodeInfo = Parser.Parser.ParseTitle(file.FolderName);
}
localEpisode = _aggregationService.Augment(localEpisode, false);
// Apply the user-chosen values.
localEpisode.Series = series;
localEpisode.Episodes = episodes;
localEpisode.Quality = file.Quality;
//TODO: Cleanup non-tracked downloads
var importDecision = new ImportDecision(localEpisode);
if (file.DownloadId.IsNullOrWhiteSpace())
if (trackedDownload == null)
{
imported.AddRange(_importApprovedEpisodes.Import(new List<ImportDecision> { importDecision }, !existingFile, null, message.ImportMode));
}
else
{
var trackedDownload = _trackedDownloadService.Find(file.DownloadId);
var importResult = _importApprovedEpisodes.Import(new List<ImportDecision> { importDecision }, true, trackedDownload.DownloadItem, message.ImportMode).First();
imported.Add(importResult);

View File

@@ -16,6 +16,11 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
public Decision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClientItem downloadClientItem)
{
if (localEpisode.FileEpisodeInfo == null)
{
return Decision.Accept();
}
if (localEpisode.FileEpisodeInfo.FullSeason)
{
_logger.Debug("Single episode file detected as containing all episodes in the season");

View File

@@ -1,60 +0,0 @@
using System.Linq;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download;
using NzbDrone.Core.History;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Qualities;
namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
{
public class GrabbedReleaseQualitySpecification : IImportDecisionEngineSpecification
{
private readonly IHistoryService _historyService;
private readonly Logger _logger;
public GrabbedReleaseQualitySpecification(IHistoryService historyService, Logger logger)
{
_historyService = historyService;
_logger = logger;
}
public Decision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClientItem downloadClientItem)
{
if (downloadClientItem == null)
{
_logger.Debug("No download client item provided, skipping.");
return Decision.Accept();
}
var grabbedHistory = _historyService.FindByDownloadId(downloadClientItem.DownloadId)
.Where(h => h.EventType == HistoryEventType.Grabbed)
.ToList();
if (grabbedHistory.Empty())
{
_logger.Debug("No grabbed history for this download client item");
return Decision.Accept();
}
var parsedReleaseName = Parser.Parser.ParseTitle(grabbedHistory.First().SourceTitle);
if (parsedReleaseName != null && parsedReleaseName.FullSeason)
{
_logger.Debug("File is part of a season pack, skipping.");
return Decision.Accept();
}
foreach (var item in grabbedHistory)
{
if (item.Quality.Quality != Quality.Unknown && item.Quality != localEpisode.Quality)
{
_logger.Debug("Quality for grabbed release ({0}) does not match the quality of the file ({1})", item.Quality, localEpisode.Quality);
return Decision.Reject("File quality does not match quality of the grabbed release");
}
}
return Decision.Accept();
}
}
}

View File

@@ -34,11 +34,19 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
if (folderInfo == null)
{
_logger.Debug("No folder ParsedEpisodeInfo, skipping check");
return Decision.Accept();
}
if (fileInfo == null)
{
_logger.Debug("No file ParsedEpisodeInfo, skipping check");
return Decision.Accept();
}
if (!folderInfo.EpisodeNumbers.Any())
{
_logger.Debug("No episode numbers in folder ParsedEpisodeInfo, skipping check");
return Decision.Accept();
}

View File

@@ -99,7 +99,11 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
string scanType = mediaInfo.Get(StreamKind.Video, 0, "ScanType");
int.TryParse(mediaInfo.Get(StreamKind.Video, 0, "Width"), out width);
int.TryParse(mediaInfo.Get(StreamKind.Video, 0, "Height"), out height);
int.TryParse(mediaInfo.Get(StreamKind.Video, 0, "BitRate"), out videoBitRate);
int.TryParse(mediaInfo.Get(StreamKind.Video, 0, "BitRate_Nominal"), out videoBitRate);
if (videoBitRate <= 0)
{
int.TryParse(mediaInfo.Get(StreamKind.Video, 0, "BitRate"), out videoBitRate);
}
decimal.TryParse(mediaInfo.Get(StreamKind.Video, 0, "FrameRate"), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out videoFrameRate);
int.TryParse(mediaInfo.Get(StreamKind.Video, 0, "BitDepth"), out videoBitDepth);

View File

@@ -49,10 +49,13 @@ namespace NzbDrone.Core.Messaging.Commands
if (typeof(IEnumerable).IsAssignableFrom(xProperty.PropertyType))
{
var xValueCollection = ((IEnumerable)xValue).Cast<object>().OrderBy(t => t);
var yValueCollection = ((IEnumerable)yValue).Cast<object>().OrderBy(t => t);
var xValueCollection = ((IEnumerable)xValue).Cast<object>();
var yValueCollection = ((IEnumerable)yValue).Cast<object>();
if (!xValueCollection.SequenceEqual(yValueCollection))
var xNotY = xValueCollection.Except(yValueCollection);
var yNotX = yValueCollection.Except(xValueCollection);
if (xNotY.Any() || yNotX.Any())
{
return false;
}

View File

@@ -43,8 +43,9 @@ namespace NzbDrone.Core.Notifications.CustomScript
environmentVariables.Add("Sonarr_Series_ImdbId", series.ImdbId ?? string.Empty);
environmentVariables.Add("Sonarr_Series_Type", series.SeriesType.ToString());
environmentVariables.Add("Sonarr_Release_EpisodeCount", remoteEpisode.Episodes.Count.ToString());
environmentVariables.Add("Sonarr_Release_SeasonNumber", remoteEpisode.ParsedEpisodeInfo.SeasonNumber.ToString());
environmentVariables.Add("Sonarr_Release_SeasonNumber", remoteEpisode.Episodes.First().SeasonNumber.ToString());
environmentVariables.Add("Sonarr_Release_EpisodeNumbers", string.Join(",", remoteEpisode.Episodes.Select(e => e.EpisodeNumber)));
environmentVariables.Add("Sonarr_Release_AbsoluteEpisodeNumbers", string.Join(",", remoteEpisode.Episodes.Select(e => e.AbsoluteEpisodeNumber)));
environmentVariables.Add("Sonarr_Release_EpisodeAirDates", string.Join(",", remoteEpisode.Episodes.Select(e => e.AirDate)));
environmentVariables.Add("Sonarr_Release_EpisodeAirDatesUtc", string.Join(",", remoteEpisode.Episodes.Select(e => e.AirDateUtc)));
environmentVariables.Add("Sonarr_Release_EpisodeTitles", string.Join("|", remoteEpisode.Episodes.Select(e => e.Title)));

View File

@@ -0,0 +1,11 @@
namespace NzbDrone.Core.Notifications.Join
{
public enum JoinPriority
{
Silent = -2,
Quiet = -1,
Normal = 0,
High = 1,
Emergency = 2
}
}

View File

@@ -94,6 +94,7 @@ namespace NzbDrone.Core.Notifications.Join
request.AddParameter("text", message);
request.AddParameter("icon", "https://cdn.rawgit.com/Sonarr/Sonarr/develop/Logo/256.png"); // Use the Sonarr logo.
request.AddParameter("smallicon", "https://cdn.rawgit.com/Sonarr/Sonarr/develop/Logo/96-Outline-White.png"); // 96x96px with outline at 88x88px on a transparent background.
request.AddParameter("priority", settings.Priority);
var response = client.ExecuteAndValidate(request);
var res = Json.Deserialize<JoinResponseModel>(response.Content);

View File

@@ -16,6 +16,12 @@ namespace NzbDrone.Core.Notifications.Join
public class JoinSettings : IProviderConfig
{
public JoinSettings()
{
Priority = (int)JoinPriority.Normal;
}
private static readonly JoinSettingsValidator Validator = new JoinSettingsValidator();
[FieldDefinition(0, Label = "API Key", HelpText = "The API Key from your Join account settings (click Join API button).", HelpLink = "https://joinjoaomgcd.appspot.com/")]
@@ -27,6 +33,9 @@ namespace NzbDrone.Core.Notifications.Join
[FieldDefinition(2, Label = "Device Names", HelpText = "Comma separated list of full or partial device names you'd like to send notifications to. If unset, all devices will receive notifications.", HelpLink = "https://joaoapps.com/join/api/")]
public string DeviceNames { get; set; }
[FieldDefinition(3, Label = "Notification Priority", Type = FieldType.Select, SelectOptions = typeof(JoinPriority))]
public int Priority { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));

View File

@@ -1,4 +1,4 @@
using FluentValidation;
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
@@ -25,7 +25,7 @@ namespace NzbDrone.Core.Notifications.Pushover
}
//TODO: Get Pushover to change our app name (or create a new app) when we have a new logo
[FieldDefinition(0, Label = "API Key", HelpLink = "https://pushover.net/apps/clone/nzbdrone")]
[FieldDefinition(0, Label = "API Key", HelpLink = "https://pushover.net/apps/clone/sonarr")]
public string ApiKey { get; set; }
[FieldDefinition(1, Label = "User Key", HelpLink = "https://pushover.net/")]

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -53,6 +53,18 @@ namespace NzbDrone.Core.Notifications.Xbmc
public void Clean(XbmcSettings settings)
{
if (!settings.AlwaysUpdate)
{
_logger.Debug("Determining if there are any active players on XBMC host: {0}", settings.Address);
var activePlayers = GetActivePlayers(settings);
if (activePlayers.Any(a => a.Type.Equals("video")))
{
_logger.Debug("Video is currently playing, skipping library cleaning");
return;
}
}
const string cleanVideoLibrary = "CleanLibrary(video)";
var command = BuildExecBuiltInCommand(cleanVideoLibrary);

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using NLog;
@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Notifications.Xbmc
if (!settings.AlwaysUpdate)
{
_logger.Debug("Determining if there are any active players on XBMC host: {0}", settings.Address);
var activePlayers = _proxy.GetActivePlayers(settings);
var activePlayers = GetActivePlayers(settings);
if (activePlayers.Any(a => a.Type.Equals("video")))
{
@@ -47,6 +47,18 @@ namespace NzbDrone.Core.Notifications.Xbmc
public void Clean(XbmcSettings settings)
{
if (!settings.AlwaysUpdate)
{
_logger.Debug("Determining if there are any active players on XBMC host: {0}", settings.Address);
var activePlayers = GetActivePlayers(settings);
if (activePlayers.Any(a => a.Type.Equals("video")))
{
_logger.Debug("Video is currently playing, skipping library cleaning");
return;
}
}
_proxy.CleanLibrary(settings);
}

View File

@@ -143,6 +143,7 @@
<Compile Include="Configuration\IConfigService.cs" />
<Compile Include="Configuration\InvalidConfigFileException.cs" />
<Compile Include="Configuration\ResetApiKeyCommand.cs" />
<Compile Include="DecisionEngine\Specifications\AlreadyImportedSpecification.cs" />
<Compile Include="Indexers\SeedConfigProvider.cs" />
<Compile Include="DataAugmentation\DailySeries\DailySeries.cs" />
<Compile Include="DataAugmentation\DailySeries\DailySeriesDataProxy.cs" />
@@ -817,7 +818,6 @@
<Compile Include="MediaFiles\EpisodeImport\Specifications\EpisodeTitleSpecification.cs" />
<Compile Include="MediaFiles\EpisodeImport\Specifications\FreeSpaceSpecification.cs" />
<Compile Include="MediaFiles\EpisodeImport\Specifications\SameFileSpecification.cs" />
<Compile Include="MediaFiles\EpisodeImport\Specifications\GrabbedReleaseQualitySpecification.cs" />
<Compile Include="MediaFiles\EpisodeImport\Specifications\MatchesFolderSpecification.cs" />
<Compile Include="MediaFiles\EpisodeImport\Specifications\FullSeasonSpecification.cs" />
<Compile Include="MediaFiles\EpisodeImport\Specifications\NotSampleSpecification.cs" />
@@ -911,6 +911,7 @@
<Compile Include="MetadataSource\ISearchForNewSeries.cs" />
<Compile Include="Notifications\Join\JoinAuthException.cs" />
<Compile Include="Notifications\Join\JoinInvalidDeviceException.cs" />
<Compile Include="Notifications\Join\JoinPriority.cs" />
<Compile Include="Notifications\Join\JoinResponseModel.cs" />
<Compile Include="Notifications\Join\Join.cs" />
<Compile Include="Notifications\Join\JoinException.cs" />

View File

@@ -22,6 +22,10 @@ namespace NzbDrone.Core.Parser
// new Regex(@"^(?:(?<absoluteepisode>\d{2,3})(?:_|-|\s|\.)+)+(?<title>.+?)(?:\W|_)+(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+)))+)",
// RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Daily episodes without title (2018-10-12, 20181012) (Strict pattern to avoid false matches)
new Regex(@"^(?<airyear>19[6-9]\d|20\d\d)(?<sep>[-]?)(?<airmonth>0\d|1[0-2])\k<sep>(?<airday>[0-2]\d|3[01])(?!\d)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Multi-Part episodes without a title (S01E05.S01E06)
new Regex(@"^(?:\W*S?(?<season>(?<!\d+)(?:\d{1,2}|\d{4})(?!\d+))(?:(?:[ex]){1,2}(?<episode>\d{1,3}(?!\d+)))+){2,}",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
@@ -95,7 +99,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{2,3}(?!\d+))(?:(?:\-|[ex]|\W[ex]|_){1,2}(?<episode>\d{2,3}(?!\d+)))*)\W?(?!\\)",
new Regex(@"^(?<title>.+?)(?:(?:[-_\W](?<![()\[!]))+S?(?<season>(?<!\d+)(?:\d{1,2})(?!\d+))(?:[ex]|\W[ex]){1,2}(?<episode>\d{2,3}(?!\d+))(?:(?:\-|[ex]|\W[ex]|_){1,2}(?<episode>\d{2,3}(?!\d+)))*)\W?(?!\\)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Episodes with a title, 4 digit season number, Single episodes (S2016E05, etc) & Multi-episode (S2016E05E06, S2016E05-06, S2016E05 E06, etc)
@@ -103,7 +107,7 @@ namespace NzbDrone.Core.Parser
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Episodes with a title, 4 digit season number, Single episodes (2016x05, etc) & Multi-episode (2016x05x06, 2016x05-06, 2016x05 x06, etc)
new Regex(@"^(?<title>.+?)(?:(?:[-_\W](?<![()\[!]))+(?<season>(?<!\d+)(?:\d{4})(?!\d+))(?:x|\Wx|_){1,2}(?<episode>\d{2,3}(?!\d+))(?:(?:\-|x|\Wx|_){1,2}(?<episode>\d{2,3}(?!\d+)))*)\W?(?!\\)",
new Regex(@"^(?<title>.+?)(?:(?:[-_\W](?<![()\[!]))+(?<season>(?<!\d+)(?:\d{4})(?!\d+))(?:x|\Wx){1,2}(?<episode>\d{2,3}(?!\d+))(?:(?:\-|x|\Wx|_){1,2}(?<episode>\d{2,3}(?!\d+)))*)\W?(?!\\)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Partial season pack
@@ -115,7 +119,7 @@ namespace NzbDrone.Core.Parser
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Mini-Series, treated as season 1, multi episodes are labelled as E1-E2
new Regex(@"^(?<title>.+?)(?:\W[e])(?<episode>\d{2,3}(?!\d+))(?:(?:\-?[e])(?<episode>\d{2,3}(?!\d+)))+",
new Regex(@"^(?<title>.+?)(?:[-._ ][e])(?<episode>\d{2,3}(?!\d+))(?:(?:\-?[e])(?<episode>\d{2,3}(?!\d+)))+",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Mini-Series, treated as season 1, episodes are labelled as Part01, Part 01, Part.1
@@ -134,8 +138,12 @@ namespace NzbDrone.Core.Parser
new Regex(@"(?:.*(?:\""|^))(?<title>.*?)(?:[-_\W](?<![()\[]))+(?:\W?Season\W?)(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:\W|_)+(?:Episode\W)(?:[-_. ]?(?<episode>(?<!\d+)\d{1,2}(?!\d+)))+",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Multi-episode with episodes in square brackets (Series Title [S01E11E12] or Series Title [S01E11-12])
new Regex(@"(?:.*(?:^))(?<title>.*?)[-._ ]+\[S(?<season>(?<!\d+)\d{2}(?!\d+))(?:[E-]{1,2}(?<episode>(?<!\d+)\d{2}(?!\d+)))+\]",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Multi-episode release with no space between series title and season (S01E11E12)
new Regex(@"(?:.*(?:^))(?<title>.*?)(?:\W?|_)S(?<season>(?<!\d+)\d{2}(?!\d+))(?:E(?<episode>(?<!\d+)\d{2}(?!\d+)))+",
new Regex(@"(?:.*(?:^))(?<title>.*?)S(?<season>(?<!\d+)\d{2}(?!\d+))(?:E(?<episode>(?<!\d+)\d{2}(?!\d+)))+",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Multi-episode with single episode numbers (S6.E1-E2, S6.E1E2, S6E1E2, etc)
@@ -162,6 +170,14 @@ namespace NzbDrone.Core.Parser
new Regex(@"^(?<title>.+?)(?:_|-|\s|\.)+S(?<season>\d{2}(?!\d+))(\W-\W)E(?<episode>(?<!\d+)\d{2}(?!\d+))(?!\\)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Anime - Title with season number - Absolute Episode Number (Title S01 - EP14)
new Regex(@"^(?<title>.+?S\d{1,2})[-_. ]{3,}(?:EP)?(?<absoluteepisode>\d{2,3}(?!\d+|[-]))",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Anime - French titles with single episode numbers, with or without leading sub group ([RlsGroup] Title - Episode 1)
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)[-_. ]+?(?:Episode[-_. ]+?)(?<absoluteepisode>\d{1}(?!\d+))",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Season only releases
new Regex(@"^(?<title>.+?)\W(?:S|Season)\W?(?<season>\d{1,2}(?!\d+))(\W+|_|$)(?<extras>EXTRAS|SUBPACK)?(?!\\)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
@@ -189,11 +205,11 @@ namespace NzbDrone.Core.Parser
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Episodes with airdate (2018.04.28)
new Regex(@"^(?<title>.+?)?\W*(?<airyear>\d{4})\W+(?<airmonth>[0-1][0-9])\W+(?<airday>[0-3][0-9])(?!\W+[0-3][0-9])",
new Regex(@"^(?<title>.+?)?\W*(?<airyear>\d{4})[-_. ]+(?<airmonth>[0-1][0-9])[-_. ]+(?<airday>[0-3][0-9])(?![-_. ]+[0-3][0-9])",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Episodes with airdate (04.28.2018)
new Regex(@"^(?<title>.+?)?\W*(?<airmonth>[0-1][0-9])\W+(?<airday>[0-3][0-9])\W+(?<airyear>\d{4})(?!\d+)",
new Regex(@"^(?<title>.+?)?\W*(?<airmonth>[0-1][0-9])[-_. ]+(?<airday>[0-3][0-9])[-_. ]+(?<airyear>\d{4})(?!\d+)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Supports 1103/1113 naming
@@ -212,6 +228,7 @@ namespace NzbDrone.Core.Parser
new Regex(@"^(?:(?<season>(?<!\d+)(?:\d{1,2})(?!\d+))(?:-(?<episode>\d{2,3}(?!\d+))))",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// TODO: THIS ONE
//Anime - Title Absolute Episode Number (e66)
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:(?:_|-|\s|\.)+(?:e|ep)(?<absoluteepisode>\d{2,3}))+.*?(?<hash>\[\w{8}\])?(?:$|\.)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
@@ -243,7 +260,7 @@ namespace NzbDrone.Core.Parser
RegexOptions.IgnoreCase | RegexOptions.Compiled)
};
private static readonly Regex[] RejectHashedReleasesRegex = new Regex[]
private static readonly Regex[] RejectHashedReleasesRegexes = new Regex[]
{
// Generic match for md5 and mixed-case hashes.
new Regex(@"^[0-9a-zA-Z]{32}", RegexOptions.Compiled),
@@ -266,7 +283,10 @@ namespace NzbDrone.Core.Parser
new Regex(@"^abc$", RegexOptions.Compiled | RegexOptions.IgnoreCase),
//b00bs - Started appearing January 2015
new Regex(@"^b00bs$", RegexOptions.Compiled | RegexOptions.IgnoreCase)
new Regex(@"^b00bs$", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// 170424_26 - Started appearing August 2018
new Regex(@"^\d{6}_\d{2}$"),
};
//Regex to detect whether the title was reversed.
@@ -278,7 +298,7 @@ namespace NzbDrone.Core.Parser
private static readonly Regex FileExtensionRegex = new Regex(@"\.[a-z0-9]{2,4}$",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex SimpleTitleRegex = new Regex(@"(?:(480|720|1080|2160)[ip]|[xh][\W_]?26[45]|DD\W?5\W1|[<>?*:|]|848x480|1280x720|1920x1080|3840x2160|4096x2160|(8|10)b(it)?)\s*",
private static readonly Regex SimpleTitleRegex = new Regex(@"(?:(480|720|1080|2160)[ip]|[xh][\W_]?26[45]|DD\W?5\W1|[<>?*:|]|848x480|1280x720|1920x1080|3840x2160|4096x2160|(8|10)b(it)?)\s*?",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex WebsitePrefixRegex = new Regex(@"^\[\s*[a-z]+(\.[a-z]+)+\s*\][- ]*|^www\.[a-z]+\.(?:com|net)[ -]*",
@@ -742,7 +762,7 @@ namespace NzbDrone.Core.Parser
var titleWithoutExtension = RemoveFileExtension(title);
if (RejectHashedReleasesRegex.Any(v => v.IsMatch(titleWithoutExtension)))
if (RejectHashedReleasesRegexes.Any(v => v.IsMatch(titleWithoutExtension)))
{
Logger.Debug("Rejected Hashed Release Title: " + title);
return false;

View File

@@ -15,8 +15,8 @@ namespace NzbDrone.Core.Parser
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(QualityParser));
private static readonly Regex SourceRegex = new Regex(@"\b(?:
(?<bluray>BluRay|Blu-Ray|HDDVD|BD)|
(?<webdl>WEB[-_. ]DL|WEBDL|WebRip|AmazonHD|iTunesHD|NetflixU?HD|WebHD|[. ]WEB[. ](?:[xh]26[45]|DD5[. ]1)|\d+0p[. ]WEB[. ]|WEB-DLMux)|
(?<bluray>BluRay|Blu-Ray|HD-?DVD|BD)|
(?<webdl>WEB[-_. ]DL|WEBDL|WebRip|AmazonHD|iTunesHD|NetflixU?HD|WebHD|[. ]WEB[. ](?:[xh]26[45]|DDP?5[. ]1)|\d+0p[. ]WEB[. ]|WEB-DLMux)|
(?<hdtv>HDTV)|
(?<bdrip>BDRip)|
(?<brrip>BRRip)|
@@ -40,7 +40,7 @@ namespace NzbDrone.Core.Parser
private static readonly Regex RealRegex = new Regex(@"\b(?<real>REAL)\b",
RegexOptions.Compiled);
private static readonly Regex ResolutionRegex = new Regex(@"\b(?:(?<R480p>480p|640x480|848x480)|(?<R576p>576p)|(?<R720p>720p|1280x720)|(?<R1080p>1080p|1920x1080|1440p)|(?<R2160p>2160p|4k[-_. ]UHD|UHD[-_. ]4k))\b",
private static readonly Regex ResolutionRegex = new Regex(@"\b(?:(?<R480p>480p|640x480|848x480)|(?<R576p>576p)|(?<R720p>720p|1280x720)|(?<R1080p>1080p|1920x1080|1440p|FHD|1080i)|(?<R2160p>2160p|4k[-_. ](?:UHD|HEVC|BD)|(?:UHD|HEVC|BD)[-_. ]4k))\b",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex CodecRegex = new Regex(@"\b(?:(?<x264>x264)|(?<h264>h264)|(?<xvidhd>XvidHD)|(?<xvid>Xvid)|(?<divx>divx))\b",

View File

@@ -1,14 +1,15 @@
using System.Collections.Generic;
using System.Collections.Generic;
namespace NzbDrone.Core.Tv
{
public static class SeriesTitleNormalizer
{
private readonly static Dictionary<int, string> PreComputedTitles = new Dictionary<int, string>
private static readonly Dictionary<int, string> PreComputedTitles = new Dictionary<int, string>
{
{ 281588, "a to z" },
{ 289260, "ad bible continues"},
{ 328534, "ap bio"}
{ 328534, "ap bio"},
{ 77904, "ateam" }
};
public static string Normalize(string title, int tvdbId)

View File

@@ -1,3 +1,4 @@
using System;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Common.Serializer;
@@ -24,5 +25,31 @@ namespace NzbDrone.Libraries.Test.JsonTests
result.ShouldBeEquivalentTo(quality, o => o.IncludingAllRuntimeProperties());
}
[Test]
public void should_log_start_snippet_on_failure()
{
try
{
Json.Deserialize<object>("asdfl kasjd fsdfs derers");
}
catch (Exception ex)
{
ex.Message.Should().Contain("snippet '<--error-->asdfl kasjd fsdfs de'");
}
}
[Test]
public void should_log_line_snippet_on_failure()
{
try
{
Json.Deserialize<object>("{ \"a\": \r\n\"b\",\r\n \"b\": \"c\", asdfl kasjd fsdfs derers vsdfsdf");
}
catch (Exception ex)
{
ex.Message.Should().Contain("snippet ' \"b\": \"c\", asdfl <--error-->kasjd fsdfs derers v'");
}
}
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -268,8 +268,6 @@ namespace NzbDrone.Mono.Disk
}
return g.gr_gid;
}
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -117,6 +117,12 @@ namespace NzbDrone.Mono.Disk
return null;
}
if (mount.StartsWith("/snap/"))
{
// Mount point for snap packages
return null;
}
var driveType = FindDriveType.Find(type);
if (name.StartsWith("/dev/") || GetFileSystems().GetValueOrDefault(type, false))

View File

@@ -30,9 +30,13 @@
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/FORCE_WHILE_BRACES_STYLE/@EntryValue">ALWAYS_ADD</s:String>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/INDENT_NESTED_FIXED_STMT/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/INDENT_NESTED_USINGS_STMT/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_ACCESSOR_ATTRIBUTE_ON_SAME_LINE_EX/@EntryValue">NEVER</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_ACCESSORHOLDER_ATTRIBUTE_ON_SAME_LINE_EX/@EntryValue">NEVER</s:String>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_FIELD_ATTRIBUTE_ON_SAME_LINE/@EntryValue">False</s:Boolean>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_FIELD_ATTRIBUTE_ON_SAME_LINE_EX/@EntryValue">NEVER</s:String>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_SIMPLE_ACCESSOR_ATTRIBUTE_ON_SAME_LINE/@EntryValue">False</s:Boolean>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_SIMPLE_ACCESSOR_ON_SINGLE_LINE/@EntryValue">False</s:Boolean>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_SIMPLE_EMBEDDED_STATEMENT_ON_SAME_LINE/@EntryValue">ALWAYS</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SIMPLE_EMBEDDED_STATEMENT_STYLE/@EntryValue">ON_SINGLE_LINE</s:String>
<s:Boolean x:Key="/Default/CodeStyle/CSharpUsing/AllowAlias/@EntryValue">False</s:Boolean>
<s:Boolean x:Key="/Default/CodeStyle/CSharpUsing/CanUseGlobalAlias/@EntryValue">False</s:Boolean>
@@ -71,7 +75,12 @@
<s:Boolean x:Key="/Default/Environment/InjectedLayers/InjectedLayerCustomization/=File_003A_003AC_003A_005CDropbox_005CGit_005CNzbDrone_005CNzbDrone_002Esln_002EDotSettings/@KeyIndexDefined">True</s:Boolean>
<s:Double x:Key="/Default/Environment/InjectedLayers/InjectedLayerCustomization/=File_003A_003AC_003A_005CDropbox_005CGit_005CNzbDrone_005CNzbDrone_002Esln_002EDotSettings/RelativePriority/@EntryValue">2</s:Double>
<s:Boolean x:Key="/Default/Environment/MemoryUsageIndicator/IsVisible/@EntryValue">False</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpAttributeForSingleLineMethodUpgrade/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpRenamePlacementToArrangementMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EAddAccessorOwnerDeclarationBracesMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002ECSharpPlaceAttributeOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateThisQualifierSettings/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/TextControl/HighlightCurrentLine/@EntryValue">True</s:Boolean>

View File

@@ -211,6 +211,7 @@ module.exports = Marionette.Layout.extend({
files : _.map(selected, function (file) {
return {
path : file.get('path'),
folderName : file.get('folderName'),
seriesId : file.get('series').id,
episodeIds : _.map(file.get('episodes'), 'id'),
quality : file.get('quality'),