1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-04-17 21:26:13 -04:00

Compare commits

..

77 Commits

Author SHA1 Message Date
Taloth Saldono
9b9597093c Fixed: Regression causing Manual Import to ignore user provided information. 2018-06-21 07:23:20 +02:00
Taloth Saldono
c92fb4d9c0 Fixed: Mini-series with multiple episode E01-E02.
fixes #2614
2018-06-17 15:57:33 +02:00
Taloth Saldono
88c659059b Added {MediaInfo AudioLanguages} {.. SubtitleLanguages}
fixes #2593
2018-06-16 22:19:33 +02:00
Taloth Saldono
d1ced600a1 Handle empty Episode list during decision maker. 2018-06-16 21:44:37 +02:00
Taloth Saldono
a5977f2752 Fixed: Manual Import not using Release Group during renamer. [develop only]
fixes #2612
2018-06-16 21:44:37 +02:00
Mark McDowall
8668e8b036 Use FolderEpisodeInfo instead of parent of FileEpisodeInfo
Fixes #2338
Fixed: Detecting some incorrect file name paring during import
2018-06-09 21:42:01 -07:00
Mark McDowall
783c27a584 Fixed: Initially pausing torrents in QBittorrent
Fixes #2599
2018-06-08 16:38:20 -07:00
Taloth Saldono
fde0409650 Fixed WithData sample length not using parameter. 2018-06-08 22:08:09 +02:00
Taloth Saldono
2ed5abf4d3 Also add as data to exception so sentry gets it. 2018-06-08 19:58:58 +02:00
Taloth Saldono
cb372f284d Log indexer response to Trace if an exception occurs. 2018-06-08 19:29:08 +02:00
Taloth Saldono
5d674a07f7 Fixed SeedConfigProvider failing on ReleasePush. 2018-05-26 00:16:34 +02:00
Mark McDowall
59e69c1839 Update bug issue template 2018-05-21 12:43:07 -07:00
Mark McDowall
89a3eec6ae Fixed: Parsing of anime releases with year and EP before episode number
Fixes #2578
2018-05-21 00:31:03 -07:00
Mark McDowall
6517f1af14 New: Treat 1440p releases as 1080p instead of 480p
Closes #2558
2018-05-20 23:33:45 -07:00
Taloth Saldono
b95c4d37d7 Fixed validation error for Seed Ratio on btn. 2018-05-20 12:34:24 +02:00
Taloth Saldono
7388e8c2c2 Fixed broken test for nested settings. 2018-05-20 10:19:52 +02:00
Taloth Saldono
b837ab41eb Fixed broken tests. 2018-05-19 22:15:19 +02:00
Taloth Saldono
de6615f586 Added a few more units. 2018-05-19 21:07:01 +02:00
Kevin Richter
0947dfc423 Stop deluge torrent when they reach stop ratio 2018-05-19 21:07:01 +02:00
Kevin Richter
002ed9e4c7 Fix parsing of entered time for seed time 2018-05-19 21:07:01 +02:00
Taloth Saldono
1a6a3038d6 Added warnings for minimum criteria for BTN. 2018-05-19 21:07:01 +02:00
Taloth Saldono
d86beb06f7 Added fancy unit indicator to fields. 2018-05-19 21:07:01 +02:00
Taloth Saldono
6df61e305d Added Seed Time and Season-Pack seed time. 2018-05-19 21:07:00 +02:00
Taloth Saldono
47018b02a8 Added nested settings for seed criteria. 2018-05-19 21:07:00 +02:00
Taloth Saldono
b6ef4d50dc Fixed more C#7. 2018-05-19 21:07:00 +02:00
Kevin Richter
2d86e44c63 New: Added advanced setting per indexer to override seed ratio limit for supported clients. 2018-05-19 21:07:00 +02:00
Taloth Saldono
69f8fc4d5e Added support for nested settings models so settings can be grouped together and reused for multiple providers. 2018-05-19 21:06:59 +02:00
Taloth Saldono
b339fcbd82 Fixed mono debug check. 2018-05-19 21:06:08 +02:00
Mark McDowall
7d1b09ac1b Update issue templates 2018-05-18 14:40:18 -07:00
Mark McDowall
55d01f620a Fixed: Setting inital state of torrents sent to QBittorrent
Fixes #2565
2018-05-16 14:00:21 -07:00
Mark McDowall
39d0d08ced Fixed: Removed old warning that Torrent Blackhole does not support magnet links 2018-05-16 13:59:33 -07:00
Mark McDowall
94f8cc9b62 Improve parsing of standard titles with junk in []
Fixed: Improve parsing of standard series releases/files with square brackets at the end
2018-05-11 19:08:47 -07:00
Mark McDowall
17b998f01f Improve parsing of non-standard date releases
Fixed: Parsing of some releases with season and episode numbers along with date
2018-05-11 18:34:34 -07:00
Mark McDowall
18415ddb30 New: Remove additional URL prefixes from release names
Closes #2542
2018-05-09 13:53:37 -07:00
Mark McDowall
2b3d0235cf Don't read media info when disabled in settings
Fixed: Don't read media info for existing files if "Analyse video files" disabled
Fixes #2549
2018-05-09 13:53:27 -07:00
Mark McDowall
c687f45ff6 Fixed: Don't try to find episodes if parsing failed 2018-05-09 13:35:56 -07:00
Mark McDowall
13f540f1f5 Fixed: Rescan series if refresh fails
Closes #2540
2018-05-02 23:34:05 -07:00
Mark McDowall
a7aff3bd9a Upgrade NLog to 4.5.3
Closes #2535
2018-05-01 23:20:54 -07:00
Taloth Saldono
03997e2a0d Switched to BigInteger for qbit eta as workaround for api bug, tyvm. 2018-04-30 22:18:42 +02:00
Mark McDowall
60c73df685 Fixed: Custom script unable to execute when release processed via /release/push API 2018-04-29 22:04:18 -07:00
Mark McDowall
b5ce3b2773 Fixed: Improved parsing of iTunes named files 2018-04-29 21:56:33 -07:00
Qstick
6c09b7dc28 Fixed: Throw SonarrStartupException if can't access AppFolder Location 2018-04-22 13:25:50 -07:00
Mark McDowall
bf1fbd7fc4 Fixed: Remove leading space from file names
Fixes #2365
2018-04-22 12:40:11 -07:00
Mark McDowall
401530db65 Fixed RemoveGrabbed tests 2018-04-22 09:40:44 -07:00
Mark McDowall
4fb7cb3a8b Fixed RemoveRejected tests 2018-04-22 09:27:07 -07:00
Mark McDowall
aba8abb176 Fixed: Suppress warnings for daily style extra files
Fixes #2503
2018-04-21 15:25:21 -07:00
Taloth Saldono
2b3956cb60 Honor x264 tag in filename if mediainfo does not conclusively indicate h264.
Closes #2500
2018-04-18 22:31:56 +02:00
Taloth Saldono
1b0647423d Updated a few harder to detect MediaInfo VideoCodec formats. 2018-04-18 22:07:38 +02:00
Taloth Saldono
5b627a8bc2 Fixed missing existingfile flag. 2018-04-15 16:52:30 +02:00
Taloth Saldono
62f7e90748 Fixed db migration issue. 2018-04-14 22:24:48 +02:00
Taloth Saldono
fd1064cb69 Fixed errors in MatchesFolderSpecification and tests. 2018-04-14 22:21:48 +02:00
Taloth Saldono
c677736a8f Fixed: Hide fallback pending releases if temporarily delayed.
Also batch changing of Pending Release Reason.
2018-04-14 22:07:08 +02:00
Taloth Saldono
67038ddd5e Cache EventAggregator Subscribers. 2018-04-14 22:07:08 +02:00
Taloth Saldono
ff8eb0b67f Added additional indexes to speed up DecisionMaker performance. 2018-04-14 22:07:08 +02:00
Taloth Saldono
70afacee3f Refactored PendingRelease logic for performance. 2018-04-14 22:07:08 +02:00
Taloth Saldono
5b0e959d3f Debounce Command Notifications. 2018-04-14 22:07:08 +02:00
Taloth Saldono
b7c5639a9d Speed up sqlite3 initialization by disabling unused features. 2018-04-14 22:07:08 +02:00
Taloth Saldono
740af2751a New: Season Search for Daily series type.
closes #755
2018-04-14 22:07:08 +02:00
Mark McDowall
81e385bebf New: Use media info during import to extract resolution for quality
Closes #448
Closes #1105
2018-04-14 22:01:24 +02:00
nivong
650d18797a Added missing bracket in issue template 2018-04-13 08:48:17 -07:00
Mark McDowall
49e91b8660 Update issue template regarding title prefixes 2018-04-11 20:45:33 -07:00
Taloth Saldono
4fb3d99153 Fixed: Updated AnimeTosho url.
closes #2501
2018-04-11 20:53:32 +02:00
Paul Kozlovitch
a29f133e5d Fixed: Pasting title into add new series search input will trigger search 2018-04-03 09:31:49 -07:00
dnnr
f277b60cc9 Fix grammar in EditProfileViewTemplate 2018-04-02 15:08:32 +02:00
Taloth Saldono
6d8eebcd30 Revised deletion of cookies. 2018-04-01 20:29:46 +02:00
Taloth Saldono
91c97ed232 Merge branch 'nfo-detector' into develop 2018-03-24 11:54:53 +01:00
margaale
13a259b473 check if mono is running with --debug arg 2018-03-24 11:49:27 +01:00
Taloth Saldono
d3f8e0ef4c Fixed: Revised handling of cookies in case of redirects. 2018-03-24 11:43:57 +01:00
Taloth Saldono
700751715b Add form param before submitting request. 2018-03-24 11:43:57 +01:00
Taloth Saldono
58e939beda Fixed: Preserve existing watched status in Kodi nfo files on metadata refreshes (not file upgrades). 2018-03-18 20:10:50 +01:00
Taloth Saldono
b19b6c93d3 New: Detect Kodi .nfo vs Scene .nfo and handle as appropriate. Rename scene .nfo to .nfo-orig only when needed. 2018-03-18 20:10:50 +01:00
Taloth Saldono
17e4a8ab66 Moved tests. 2018-03-18 20:10:50 +01:00
Taloth Saldono
3573e631c8 Fixed: Recycle Metadata files on episode removal. 2018-03-18 20:10:50 +01:00
Taloth Saldono
616454edb4 Fixed: Preserve existing watched status in Kodi nfo files on metadata refreshes (not file upgrades). 2018-03-01 20:36:07 +01:00
Taloth Saldono
d284969379 New: Detect Kodi .nfo vs Scene .nfo and handle as appropriate. Rename scene .nfo to .nfo-orig only when needed. 2018-03-01 20:35:12 +01:00
Taloth Saldono
f0eba619f3 Moved tests. 2018-03-01 18:30:09 +01:00
Taloth Saldono
448cfff769 Fixed: Recycle Metadata files on episode removal. 2018-03-01 18:30:08 +01:00
199 changed files with 3604 additions and 1027 deletions

View File

@@ -3,6 +3,7 @@ Before opening a new issue, please ensure:
- You use the forums for support/questions
- You search for existing bugs/feature requests
- Remove extraneous template details
- Do not prefix title with type of issue (Feature Request, Bug, etc.) The appropriate labels will be added during triage.
-->
## Support / Questions

28
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,28 @@
---
name: Bug report
about: Create a report to help us improve Sonarr
---
**Describe the bug**
A clear and concise description of what the bug is.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Logs**
Link to debug or trace log files.
**System Information**
- Sonarr Version: [e.g. 2.0.0.1]
- Operating System: [e.g. Windows 10]
- .net Framework (Windows) or mono (macOS/Linux) Version: [e.g. 4.5 or 5.12]
**UI Bugs:**
- OS: [e.g. Windows]
- Browser: [e.g. chrome, firefox]
- Version: [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,14 @@
---
name: Feature request
about: Suggest an idea for Sonarr
---
**Describe the problem**
A clear and concise description of the problem you're looking to solve.
**Describe any solutions you think might work**
A clear and concise description of any solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -0,0 +1,7 @@
---
name: Other issues
about: How to get support or ask questions
---
Please use https://forums.sonarr.tv/ for support. Support requests or questions will be redirected to the forums and the issue will be closed.

View File

@@ -52,10 +52,14 @@
</PropertyGroup>
<ItemGroup>
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<HintPath>..\packages\NLog.4.5.0-rc06\lib\net40-client\NLog.dll</HintPath>
<HintPath>..\packages\NLog.4.5.3\lib\net40-client\NLog.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Configuration" />
<Reference Include="System.Core" />
<Reference Include="System.Runtime.Serialization" />
<Reference Include="System.ServiceModel" />
<Reference Include="System.Transactions" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="NLog" version="4.5.0-rc06" targetFramework="net40" />
<package id="NLog" version="4.5.3" targetFramework="net40" />
</packages>

View File

@@ -21,19 +21,32 @@ namespace NzbDrone.Api.Test.ClientSchemaTests
public void schema_should_have_proper_fields()
{
var model = new TestModel
{
FirstName = "Bob",
LastName = "Poop"
};
{
FirstName = "Bob",
LastName = "Poop"
};
var schema = SchemaBuilder.ToSchema(model);
schema.Should().Contain(c => c.Order == 1 && c.Name == "LastName" && c.Label == "Last Name" && c.HelpText == "Your Last Name" && (string) c.Value == "Poop");
schema.Should().Contain(c => c.Order == 0 && c.Name == "FirstName" && c.Label == "First Name" && c.HelpText == "Your First Name" && (string) c.Value == "Bob");
schema.Should().Contain(c => c.Order == 1 && c.Name == "LastName" && c.Label == "Last Name" && c.HelpText == "Your Last Name" && (string)c.Value == "Poop");
schema.Should().Contain(c => c.Order == 0 && c.Name == "FirstName" && c.Label == "First Name" && c.HelpText == "Your First Name" && (string)c.Value == "Bob");
}
}
[Test]
public void schema_should_have_nested_fields()
{
var model = new NestedTestModel();
model.Name.FirstName = "Bob";
model.Name.LastName = "Poop";
var schema = SchemaBuilder.ToSchema(model);
schema.Should().Contain(c => c.Order == 0 && c.Name == "Name.FirstName" && c.Label == "First Name" && c.HelpText == "Your First Name" && (string)c.Value == "Bob");
schema.Should().Contain(c => c.Order == 1 && c.Name == "Name.LastName" && c.Label == "Last Name" && c.HelpText == "Your Last Name" && (string)c.Value == "Poop");
schema.Should().Contain(c => c.Order == 2 && c.Name == "Quote" && c.Label == "Quote" && c.HelpText == "Your Favorite Quote");
}
}
public class TestModel
{
@@ -45,4 +58,13 @@ namespace NzbDrone.Api.Test.ClientSchemaTests
public string Other { get; set; }
}
}
public class NestedTestModel
{
[FieldDefinition(0)]
public TestModel Name { get; set; } = new TestModel();
[FieldDefinition(1, Label = "Quote", HelpText = "Your Favorite Quote")]
public string Quote { get; set; }
}
}

View File

@@ -7,11 +7,17 @@ namespace NzbDrone.Api.ClientSchema
public int Order { get; set; }
public string Name { get; set; }
public string Label { get; set; }
public string Unit { get; set; }
public string HelpText { get; set; }
public string HelpLink { get; set; }
public object Value { get; set; }
public string Type { get; set; }
public bool Advanced { get; set; }
public List<SelectOption> SelectOptions { get; set; }
public Field Clone()
{
return (Field)MemberwiseClone();
}
}
}
}

View File

@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace NzbDrone.Api.ClientSchema
{
public class FieldMapping
{
public Field Field { get; set; }
public Type PropertyType { get; set; }
public Func<object, object> GetterFunc { get; set; }
public Action<object, object> SetterFunc { get; set; }
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Newtonsoft.Json.Linq;
using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.Extensions;
@@ -11,45 +12,22 @@ namespace NzbDrone.Api.ClientSchema
{
public static class SchemaBuilder
{
private static Dictionary<Type, FieldMapping[]> _mappings = new Dictionary<Type, FieldMapping[]>();
public static List<Field> ToSchema(object model)
{
Ensure.That(model, () => model).IsNotNull();
var properties = model.GetType().GetSimpleProperties();
var mappings = GetFieldMappings(model.GetType());
var result = new List<Field>(properties.Count);
var result = new List<Field>(mappings.Length);
foreach (var propertyInfo in properties)
foreach (var mapping in mappings)
{
var fieldAttribute = propertyInfo.GetAttribute<FieldDefinitionAttribute>(false);
var field = mapping.Field.Clone();
field.Value = mapping.GetterFunc(model);
if (fieldAttribute != null)
{
var field = new Field
{
Name = propertyInfo.Name,
Label = fieldAttribute.Label,
HelpText = fieldAttribute.HelpText,
HelpLink = fieldAttribute.HelpLink,
Order = fieldAttribute.Order,
Advanced = fieldAttribute.Advanced,
Type = fieldAttribute.Type.ToString().ToLowerInvariant()
};
var value = propertyInfo.GetValue(model, null);
if (value != null)
{
field.Value = value;
}
if (fieldAttribute.Type == FieldType.Select)
{
field.SelectOptions = GetSelectOptions(fieldAttribute.SelectOptions);
}
result.Add(field);
}
result.Add(field);
}
return result.OrderBy(r => r.Order).ToList();
@@ -59,81 +37,16 @@ namespace NzbDrone.Api.ClientSchema
{
Ensure.That(targetType, () => targetType).IsNotNull();
var properties = targetType.GetSimpleProperties();
var mappings = GetFieldMappings(targetType);
var target = Activator.CreateInstance(targetType);
foreach (var propertyInfo in properties)
foreach (var mapping in mappings)
{
var fieldAttribute = propertyInfo.GetAttribute<FieldDefinitionAttribute>(false);
var propertyType = mapping.PropertyType;
var field = fields.Find(f => f.Name == mapping.Field.Name);
if (fieldAttribute != null)
{
var field = fields.Find(f => f.Name == propertyInfo.Name);
if (propertyInfo.PropertyType == typeof(int))
{
var value = field.Value.ToString().ParseInt32();
propertyInfo.SetValue(target, value ?? 0, null);
}
else if (propertyInfo.PropertyType == typeof(long))
{
var value = field.Value.ToString().ParseInt64();
propertyInfo.SetValue(target, value ?? 0, null);
}
else if (propertyInfo.PropertyType == typeof(int?))
{
var value = field.Value.ToString().ParseInt32();
propertyInfo.SetValue(target, value, null);
}
else if (propertyInfo.PropertyType == typeof(Nullable<Int64>))
{
var value = field.Value.ToString().ParseInt64();
propertyInfo.SetValue(target, value, null);
}
else if (propertyInfo.PropertyType == typeof(IEnumerable<int>))
{
IEnumerable<int> value;
if (field.Value.GetType() == typeof(JArray))
{
value = ((JArray)field.Value).Select(s => s.Value<int>());
}
else
{
value = field.Value.ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(s => Convert.ToInt32(s));
}
propertyInfo.SetValue(target, value, null);
}
else if (propertyInfo.PropertyType == typeof(IEnumerable<string>))
{
IEnumerable<string> value;
if (field.Value.GetType() == typeof(JArray))
{
value = ((JArray)field.Value).Select(s => s.Value<string>());
}
else
{
value = field.Value.ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
}
propertyInfo.SetValue(target, value, null);
}
else
{
propertyInfo.SetValue(target, field.Value, null);
}
}
mapping.SetterFunc(target, field.Value);
}
return target;
@@ -145,6 +58,84 @@ namespace NzbDrone.Api.ClientSchema
return (T)ReadFromSchema(fields, typeof(T));
}
// Ideally this function should begin a System.Linq.Expression expression tree since it's faster.
// But it's probably not needed till performance issues pop up.
public static FieldMapping[] GetFieldMappings(Type type)
{
lock (_mappings)
{
FieldMapping[] result;
if (!_mappings.TryGetValue(type, out result))
{
result = GetFieldMapping(type, "", v => v);
// Renumber al the field Orders since nested settings will have dupe Orders.
for (int i = 0; i < result.Length; i++)
{
result[i].Field.Order = i;
}
_mappings[type] = result;
}
return result;
}
}
private static FieldMapping[] GetFieldMapping(Type type, string prefix, Func<object, object> targetSelector)
{
var result = new List<FieldMapping>();
foreach (var property in GetProperties(type))
{
var propertyInfo = property.Item1;
if (propertyInfo.PropertyType.IsSimpleType())
{
var fieldAttribute = property.Item2;
var field = new Field
{
Name = prefix + propertyInfo.Name,
Label = fieldAttribute.Label,
Unit = fieldAttribute.Unit,
HelpText = fieldAttribute.HelpText,
HelpLink = fieldAttribute.HelpLink,
Order = fieldAttribute.Order,
Advanced = fieldAttribute.Advanced,
Type = fieldAttribute.Type.ToString().ToLowerInvariant()
};
if (fieldAttribute.Type == FieldType.Select)
{
field.SelectOptions = GetSelectOptions(fieldAttribute.SelectOptions);
}
var valueConverter = GetValueConverter(propertyInfo.PropertyType);
result.Add(new FieldMapping
{
Field = field,
PropertyType = propertyInfo.PropertyType,
GetterFunc = t => propertyInfo.GetValue(targetSelector(t), null),
SetterFunc = (t, v) => propertyInfo.SetValue(targetSelector(t), valueConverter(v), null)
});
}
else
{
result.AddRange(GetFieldMapping(propertyInfo.PropertyType, propertyInfo.Name + ".", t => propertyInfo.GetValue(targetSelector(t), null)));
}
}
return result.ToArray();
}
private static Tuple<PropertyInfo, FieldDefinitionAttribute>[] GetProperties(Type type)
{
return type.GetProperties()
.Select(v => Tuple.Create(v, v.GetAttribute<FieldDefinitionAttribute>(false)))
.Where(v => v.Item2 != null)
.OrderBy(v => v.Item2.Order)
.ToArray();
}
private static List<SelectOption> GetSelectOptions(Type selectOptions)
{
var options = from Enum e in Enum.GetValues(selectOptions)
@@ -152,5 +143,73 @@ namespace NzbDrone.Api.ClientSchema
return options.OrderBy(o => o.Value).ToList();
}
private static Func<object, object> GetValueConverter(Type propertyType)
{
if (propertyType == typeof(int))
{
return fieldValue => fieldValue?.ToString().ParseInt32() ?? 0;
}
else if (propertyType == typeof(long))
{
return fieldValue => fieldValue?.ToString().ParseInt64() ?? 0;
}
else if (propertyType == typeof(double))
{
return fieldValue => fieldValue?.ToString().ParseDouble() ?? 0.0;
}
else if (propertyType == typeof(int?))
{
return fieldValue => fieldValue?.ToString().ParseInt32();
}
else if (propertyType == typeof(Int64?))
{
return fieldValue => fieldValue?.ToString().ParseInt64();
}
else if (propertyType == typeof(double?))
{
return fieldValue => fieldValue?.ToString().ParseDouble();
}
else if (propertyType == typeof(IEnumerable<int>))
{
return fieldValue =>
{
if (fieldValue.GetType() == typeof(JArray))
{
return ((JArray)fieldValue).Select(s => s.Value<int>());
}
else
{
return fieldValue.ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(s => Convert.ToInt32(s));
}
};
}
else if (propertyType == typeof(IEnumerable<string>))
{
return fieldValue =>
{
if (fieldValue.GetType() == typeof(JArray))
{
return ((JArray)fieldValue).Select(s => s.Value<string>());
}
else
{
return fieldValue.ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
}
};
}
else
{
return fieldValue => fieldValue;
}
}
}
}

View File

@@ -4,6 +4,7 @@ using System.Linq;
using NzbDrone.Api.Extensions;
using NzbDrone.Api.Validation;
using NzbDrone.Common;
using NzbDrone.Common.TPL;
using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
@@ -17,6 +18,8 @@ namespace NzbDrone.Api.Commands
{
private readonly IManageCommandQueue _commandQueueManager;
private readonly IServiceFactory _serviceFactory;
private readonly Debouncer _debouncer;
private readonly Dictionary<int, CommandResource> _pendingUpdates;
public CommandModule(IManageCommandQueue commandQueueManager,
IBroadcastSignalRMessage signalRBroadcaster,
@@ -31,6 +34,10 @@ namespace NzbDrone.Api.Commands
GetResourceAll = GetStartedCommands;
PostValidator.RuleFor(c => c.Name).NotBlank();
_debouncer = new Debouncer(SendUpdates, TimeSpan.FromSeconds(0.1));
_pendingUpdates = new Dictionary<int, CommandResource>();
}
private CommandResource GetCommand(int id)
@@ -59,8 +66,26 @@ namespace NzbDrone.Api.Commands
{
if (message.Command.Body.SendUpdatesToClient)
{
BroadcastResourceChange(ModelAction.Updated, message.Command.ToResource());
lock (_pendingUpdates)
{
_pendingUpdates[message.Command.Id] = message.Command.ToResource();
}
_debouncer.Execute();
}
}
private void SendUpdates()
{
lock (_pendingUpdates)
{
var pendingUpdates = _pendingUpdates.Values.ToArray();
_pendingUpdates.Clear();
foreach (var pendingUpdate in pendingUpdates)
{
BroadcastResourceChange(ModelAction.Updated, pendingUpdate);
}
}
}
}
}
}

View File

@@ -1,13 +1,16 @@
using Nancy;
using Nancy.ModelBinding;
using System.Collections.Generic;
using System.Linq;
using FluentValidation;
using Nancy;
using Nancy.ModelBinding;
using NLog;
using NzbDrone.Api.Extensions;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download;
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Api.Extensions;
using NLog;
namespace NzbDrone.Api.Indexers
{
@@ -15,14 +18,17 @@ namespace NzbDrone.Api.Indexers
{
private readonly IMakeDownloadDecision _downloadDecisionMaker;
private readonly IProcessDownloadDecisions _downloadDecisionProcessor;
private readonly IIndexerFactory _indexerFactory;
private readonly Logger _logger;
public ReleasePushModule(IMakeDownloadDecision downloadDecisionMaker,
IProcessDownloadDecisions downloadDecisionProcessor,
IIndexerFactory indexerFactory,
Logger logger)
{
_downloadDecisionMaker = downloadDecisionMaker;
_downloadDecisionProcessor = downloadDecisionProcessor;
_indexerFactory = indexerFactory;
_logger = logger;
Post["/push"] = x => ProcessRelease(this.Bind<ReleaseResource>());
@@ -41,10 +47,47 @@ namespace NzbDrone.Api.Indexers
info.Guid = "PUSH-" + info.DownloadUrl;
ResolveIndexer(info);
var decisions = _downloadDecisionMaker.GetRssDecision(new List<ReleaseInfo> { info });
_downloadDecisionProcessor.ProcessDecisions(decisions);
return MapDecisions(decisions).First().AsResponse();
}
private void ResolveIndexer(ReleaseInfo release)
{
if (release.IndexerId == 0 && release.Indexer.IsNotNullOrWhiteSpace())
{
var indexer = _indexerFactory.All().FirstOrDefault(v => v.Name == release.Indexer);
if (indexer != null)
{
release.IndexerId = indexer.Id;
_logger.Debug("Push Release {0} associated with indexer {1} - {2}.", release.Title, release.IndexerId, release.Indexer);
}
else
{
_logger.Debug("Push Release {0} not associated with unknown indexer {1}.", release.Title, release.Indexer);
}
}
else if (release.IndexerId != 0 && release.Indexer.IsNullOrWhiteSpace())
{
try
{
var indexer = _indexerFactory.Get(release.IndexerId);
release.Indexer = indexer.Name;
_logger.Debug("Push Release {0} associated with indexer {1} - {2}.", release.Title, release.IndexerId, release.Indexer);
}
catch (ModelNotFoundException)
{
_logger.Debug("Push Release {0} not associated with unknown indexer {0}.", release.Title, release.IndexerId);
release.IndexerId = 0;
}
}
else
{
_logger.Debug("Push Release {0} not associated with an indexer.", release.Title);
}
}
}
}

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

@@ -70,12 +70,13 @@
<Private>True</Private>
</Reference>
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<HintPath>..\packages\NLog.4.5.0-rc06\lib\net40-client\NLog.dll</HintPath>
<HintPath>..\packages\NLog.4.5.3\lib\net40-client\NLog.dll</HintPath>
</Reference>
<Reference Include="NodaTime, Version=1.3.0.0, Culture=neutral, PublicKeyToken=4226afe0d9b296d1, processorArchitecture=MSIL">
<HintPath>..\packages\Ical.Net.2.2.32\lib\net40\NodaTime.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Configuration" />
<Reference Include="System.Core" />
<Reference Include="System.Data" />
<Reference Include="Microsoft.CSharp" />
@@ -83,6 +84,10 @@
<SpecificVersion>False</SpecificVersion>
<HintPath>..\Libraries\Sqlite\System.Data.SQLite.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.Serialization" />
<Reference Include="System.ServiceModel" />
<Reference Include="System.Transactions" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\NzbDrone.Common\Properties\SharedAssemblyInfo.cs">
@@ -99,6 +104,7 @@
<Compile Include="Calendar\CalendarModule.cs" />
<Compile Include="ClientSchema\Field.cs" />
<Compile Include="ClientSchema\FieldDefinitionAttribute.cs" />
<Compile Include="ClientSchema\FieldMapping.cs" />
<Compile Include="ClientSchema\SchemaBuilder.cs" />
<Compile Include="ClientSchema\SchemaDeserializer.cs" />
<Compile Include="ClientSchema\SelectOption.cs" />

View File

@@ -210,7 +210,12 @@ namespace NzbDrone.Api
protected void VerifyValidationResult(ValidationResult validationResult, bool includeWarnings)
{
var result = new NzbDroneValidationResult(validationResult.Errors);
var result = validationResult as NzbDroneValidationResult;
if (result == null)
{
result = new NzbDroneValidationResult(validationResult.Errors);
}
if (includeWarnings && (!result.IsValid || result.HasWarnings))
{

View File

@@ -6,5 +6,5 @@
<package id="Nancy.Authentication.Basic" version="1.4.1" targetFramework="net40" />
<package id="Nancy.Authentication.Forms" version="1.4.1" targetFramework="net40" />
<package id="Newtonsoft.Json" version="9.0.1" targetFramework="net40" />
<package id="NLog" version="4.5.0-rc06" targetFramework="net40" />
<package id="NLog" version="4.5.3" targetFramework="net40" />
</packages>

View File

@@ -47,19 +47,25 @@
<Reference Include="FluentAssertions.Core, Version=4.19.0.0, Culture=neutral, PublicKeyToken=33f2691a05b67b6a, processorArchitecture=MSIL">
<HintPath>..\packages\FluentAssertions.4.19.0\lib\net40\FluentAssertions.Core.dll</HintPath>
</Reference>
<Reference Include="Microsoft.CSharp" />
<Reference Include="Moq, Version=4.0.10827.0, Culture=neutral, PublicKeyToken=69f491c39445e920, processorArchitecture=MSIL">
<HintPath>..\packages\Moq.4.0.10827\lib\NET40\Moq.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<HintPath>..\packages\NLog.4.5.0-rc06\lib\net40-client\NLog.dll</HintPath>
<HintPath>..\packages\NLog.4.5.3\lib\net40-client\NLog.dll</HintPath>
</Reference>
<Reference Include="nunit.framework, Version=3.6.0.0, Culture=neutral, PublicKeyToken=2638cd05610744eb, processorArchitecture=MSIL">
<HintPath>..\packages\NUnit.3.6.0\lib\net40\nunit.framework.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Configuration" />
<Reference Include="System.Core" />
<Reference Include="System.Data" />
<Reference Include="System.Runtime.Serialization" />
<Reference Include="System.ServiceModel" />
<Reference Include="System.ServiceProcess" />
<Reference Include="System.Transactions" />
<Reference Include="System.Xml" />
<Reference Include="System.Xml.Linq" />
</ItemGroup>

View File

@@ -3,6 +3,6 @@
<package id="FluentAssertions" version="4.19.0" targetFramework="net40" />
<package id="Moq" version="4.0.10827" targetFramework="net40" />
<package id="NBuilder" version="4.0.0" targetFramework="net40" />
<package id="NLog" version="4.5.0-rc06" targetFramework="net40" />
<package id="NLog" version="4.5.3" targetFramework="net40" />
<package id="NUnit" version="3.6.0" targetFramework="net40" />
</packages>

View File

@@ -45,16 +45,20 @@
<HintPath>..\packages\FluentAssertions.4.19.0\lib\net40\FluentAssertions.Core.dll</HintPath>
</Reference>
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<HintPath>..\packages\NLog.4.5.0-rc06\lib\net40-client\NLog.dll</HintPath>
<HintPath>..\packages\NLog.4.5.3\lib\net40-client\NLog.dll</HintPath>
</Reference>
<Reference Include="nunit.framework, Version=3.6.0.0, Culture=neutral, PublicKeyToken=2638cd05610744eb, processorArchitecture=MSIL">
<HintPath>..\packages\NUnit.3.6.0\lib\net40\nunit.framework.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Configuration" />
<Reference Include="System.Core" />
<Reference Include="System.Data" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="System.Drawing" />
<Reference Include="System.Runtime.Serialization" />
<Reference Include="System.ServiceModel" />
<Reference Include="System.Transactions" />
<Reference Include="System.Xml" />
<Reference Include="System.Xml.Linq" />
<Reference Include="Microsoft.CSharp" />

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="FluentAssertions" version="4.19.0" targetFramework="net40" />
<package id="NLog" version="4.5.0-rc06" targetFramework="net40" />
<package id="NLog" version="4.5.3" targetFramework="net40" />
<package id="NUnit" version="3.6.0" targetFramework="net40" />
<package id="Selenium.Support" version="3.2.0" targetFramework="net40" />
<package id="Selenium.WebDriver" version="3.2.0" targetFramework="net40" />

View File

@@ -285,19 +285,96 @@ namespace NzbDrone.Common.Test.Http
response.Resource.Headers.Should().NotContainKey("Cookie");
}
[Test]
public void should_not_store_request_cookie()
{
var requestGet = new HttpRequest($"http://{_httpBinHost}/get");
requestGet.Cookies.Add("my", "cookie");
requestGet.AllowAutoRedirect = false;
requestGet.StoreRequestCookie = false;
requestGet.StoreResponseCookie = false;
var responseGet = Subject.Get<HttpBinResource>(requestGet);
var requestCookies = new HttpRequest($"http://{_httpBinHost}/cookies");
requestCookies.AllowAutoRedirect = false;
var responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().BeEmpty();
ExceptionVerification.IgnoreErrors();
}
[Test]
public void should_store_request_cookie()
{
var requestGet = new HttpRequest($"http://{_httpBinHost}/get");
requestGet.Cookies.Add("my", "cookie");
requestGet.AllowAutoRedirect = false;
requestGet.StoreRequestCookie.Should().BeTrue();
requestGet.StoreResponseCookie = false;
var responseGet = Subject.Get<HttpBinResource>(requestGet);
var requestCookies = new HttpRequest($"http://{_httpBinHost}/cookies");
requestCookies.AllowAutoRedirect = false;
var responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
ExceptionVerification.IgnoreErrors();
}
[Test]
public void should_delete_request_cookie()
{
var requestDelete = new HttpRequest($"http://{_httpBinHost}/cookies/delete?my");
requestDelete.Cookies.Add("my", "cookie");
requestDelete.AllowAutoRedirect = true;
requestDelete.StoreRequestCookie = false;
requestDelete.StoreResponseCookie = false;
// Delete and redirect since that's the only way to check the internal temporary cookie container
var responseCookies = Subject.Get<HttpCookieResource>(requestDelete);
responseCookies.Resource.Cookies.Should().BeEmpty();
}
[Test]
public void should_clear_request_cookie()
{
var requestSet = new HttpRequest($"http://{_httpBinHost}/cookies");
requestSet.Cookies.Add("my", "cookie");
requestSet.AllowAutoRedirect = false;
requestSet.StoreRequestCookie = true;
requestSet.StoreResponseCookie = false;
var responseSet = Subject.Get<HttpCookieResource>(requestSet);
var requestClear = new HttpRequest($"http://{_httpBinHost}/cookies");
requestClear.Cookies.Add("my", null);
requestClear.AllowAutoRedirect = false;
requestClear.StoreRequestCookie = true;
requestClear.StoreResponseCookie = false;
var responseClear = Subject.Get<HttpCookieResource>(requestClear);
responseClear.Resource.Cookies.Should().BeEmpty();
}
[Test]
public void should_not_store_response_cookie()
{
var requestSet = new HttpRequest($"http://{_httpBinHost}/cookies/set?my=cookie");
requestSet.AllowAutoRedirect = false;
requestSet.StoreRequestCookie = false;
requestSet.StoreResponseCookie.Should().BeFalse();
var responseSet = Subject.Get(requestSet);
var request = new HttpRequest($"http://{_httpBinHost}/get");
var requestCookies = new HttpRequest($"http://{_httpBinHost}/cookies");
var response = Subject.Get<HttpBinResource>(request);
var responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
response.Resource.Headers.Should().NotContainKey("Cookie");
responseCookies.Resource.Cookies.Should().BeEmpty();
ExceptionVerification.IgnoreErrors();
}
@@ -307,19 +384,31 @@ namespace NzbDrone.Common.Test.Http
{
var requestSet = new HttpRequest($"http://{_httpBinHost}/cookies/set?my=cookie");
requestSet.AllowAutoRedirect = false;
requestSet.StoreRequestCookie = false;
requestSet.StoreResponseCookie = true;
var responseSet = Subject.Get(requestSet);
var request = new HttpRequest($"http://{_httpBinHost}/get");
var requestCookies = new HttpRequest($"http://{_httpBinHost}/cookies");
var response = Subject.Get<HttpBinResource>(request);
var responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
response.Resource.Headers.Should().ContainKey("Cookie");
responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
var cookie = response.Resource.Headers["Cookie"].ToString();
ExceptionVerification.IgnoreErrors();
}
cookie.Should().Contain("my=cookie");
[Test]
public void should_temp_store_response_cookie()
{
var requestSet = new HttpRequest($"http://{_httpBinHost}/cookies/set?my=cookie");
requestSet.AllowAutoRedirect = true;
requestSet.StoreRequestCookie = false;
requestSet.StoreResponseCookie.Should().BeFalse();
var responseSet = Subject.Get<HttpCookieResource>(requestSet);
// Set and redirect since that's the only way to check the internal temporary cookie container
responseSet.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
ExceptionVerification.IgnoreErrors();
}
@@ -328,21 +417,129 @@ namespace NzbDrone.Common.Test.Http
public void should_overwrite_response_cookie()
{
var requestSet = new HttpRequest($"http://{_httpBinHost}/cookies/set?my=cookie");
requestSet.Cookies.Add("my", "oldcookie");
requestSet.AllowAutoRedirect = false;
requestSet.StoreRequestCookie = false;
requestSet.StoreResponseCookie = true;
requestSet.Cookies["my"] = "oldcookie";
var responseSet = Subject.Get(requestSet);
var request = new HttpRequest($"http://{_httpBinHost}/get");
var requestCookies = new HttpRequest($"http://{_httpBinHost}/cookies");
var response = Subject.Get<HttpBinResource>(request);
var responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
response.Resource.Headers.Should().ContainKey("Cookie");
responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
var cookie = response.Resource.Headers["Cookie"].ToString();
ExceptionVerification.IgnoreErrors();
}
cookie.Should().Contain("my=cookie");
[Test]
public void should_overwrite_temp_response_cookie()
{
var requestSet = new HttpRequest($"http://{_httpBinHost}/cookies/set?my=cookie");
requestSet.Cookies.Add("my", "oldcookie");
requestSet.AllowAutoRedirect = true;
requestSet.StoreRequestCookie = true;
requestSet.StoreResponseCookie = false;
var responseSet = Subject.Get<HttpCookieResource>(requestSet);
responseSet.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
var requestCookies = new HttpRequest($"http://{_httpBinHost}/cookies");
var responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "oldcookie");
ExceptionVerification.IgnoreErrors();
}
[Test]
public void should_not_delete_response_cookie()
{
var requestCookies = new HttpRequest($"http://{_httpBinHost}/cookies");
requestCookies.Cookies.Add("my", "cookie");
requestCookies.AllowAutoRedirect = false;
requestCookies.StoreRequestCookie = true;
requestCookies.StoreResponseCookie = false;
var responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
var requestDelete = new HttpRequest($"http://{_httpBinHost}/cookies/delete?my");
requestDelete.AllowAutoRedirect = false;
requestDelete.StoreRequestCookie = false;
requestDelete.StoreResponseCookie = false;
var responseDelete = Subject.Get(requestDelete);
requestCookies = new HttpRequest($"http://{_httpBinHost}/cookies");
requestCookies.StoreRequestCookie = false;
requestCookies.StoreResponseCookie = false;
responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
ExceptionVerification.IgnoreErrors();
}
[Test]
public void should_delete_response_cookie()
{
var requestCookies = new HttpRequest($"http://{_httpBinHost}/cookies");
requestCookies.Cookies.Add("my", "cookie");
requestCookies.AllowAutoRedirect = false;
requestCookies.StoreRequestCookie = true;
requestCookies.StoreResponseCookie = false;
var responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
var requestDelete = new HttpRequest($"http://{_httpBinHost}/cookies/delete?my");
requestDelete.AllowAutoRedirect = false;
requestDelete.StoreRequestCookie = false;
requestDelete.StoreResponseCookie = true;
var responseDelete = Subject.Get(requestDelete);
requestCookies = new HttpRequest($"http://{_httpBinHost}/cookies");
requestCookies.StoreRequestCookie = false;
requestCookies.StoreResponseCookie = false;
responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().BeEmpty();
ExceptionVerification.IgnoreErrors();
}
[Test]
public void should_delete_temp_response_cookie()
{
var requestCookies = new HttpRequest($"http://{_httpBinHost}/cookies");
requestCookies.Cookies.Add("my", "cookie");
requestCookies.AllowAutoRedirect = false;
requestCookies.StoreRequestCookie = true;
requestCookies.StoreResponseCookie = false;
var responseCookies = Subject.Get<HttpCookieResource>(requestCookies);
responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
var requestDelete = new HttpRequest($"http://{_httpBinHost}/cookies/delete?my");
requestDelete.AllowAutoRedirect = true;
requestDelete.StoreRequestCookie = false;
requestDelete.StoreResponseCookie = false;
var responseDelete = Subject.Get<HttpCookieResource>(requestDelete);
responseDelete.Resource.Cookies.Should().BeEmpty();
requestCookies = new HttpRequest($"http://{_httpBinHost}/cookies");
requestCookies.StoreRequestCookie = false;
requestCookies.StoreResponseCookie = false;
responseCookies.Resource.Cookies.Should().HaveCount(1).And.Contain("my", "cookie");
ExceptionVerification.IgnoreErrors();
}
@@ -454,4 +651,9 @@ namespace NzbDrone.Common.Test.Http
public string Url { get; set; }
public string Data { get; set; }
}
public class HttpCookieResource
{
public Dictionary<string, string> Cookies { get; set; }
}
}

View File

@@ -48,16 +48,20 @@
<Private>True</Private>
</Reference>
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<HintPath>..\packages\NLog.4.5.0-rc06\lib\net40-client\NLog.dll</HintPath>
<HintPath>..\packages\NLog.4.5.3\lib\net40-client\NLog.dll</HintPath>
</Reference>
<Reference Include="nunit.framework, Version=3.6.0.0, Culture=neutral, PublicKeyToken=2638cd05610744eb, processorArchitecture=MSIL">
<HintPath>..\packages\NUnit.3.6.0\lib\net40\nunit.framework.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Configuration" />
<Reference Include="System.Core" />
<Reference Include="System.Data" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="System.Runtime.Serialization" />
<Reference Include="System.ServiceModel" />
<Reference Include="System.ServiceProcess" />
<Reference Include="System.Transactions" />
<Reference Include="System.Xml" />
<Reference Include="System.Xml.Linq" />
<Reference Include="Microsoft.CSharp" />

View File

@@ -2,6 +2,6 @@
<packages>
<package id="FluentAssertions" version="4.19.0" targetFramework="net40" />
<package id="Moq" version="4.0.10827" targetFramework="net40" />
<package id="NLog" version="4.5.0-rc06" targetFramework="net40" />
<package id="NLog" version="4.5.3" targetFramework="net40" />
<package id="NUnit" version="3.6.0" targetFramework="net40" />
</packages>

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Security.AccessControl;
using System.Security.Principal;
using NLog;
@@ -28,7 +28,15 @@ namespace NzbDrone.Common.EnvironmentInfo
public void Register()
{
_diskProvider.EnsureFolder(_appFolderInfo.AppDataFolder);
try
{
_diskProvider.EnsureFolder(_appFolderInfo.AppDataFolder);
}
catch (UnauthorizedAccessException)
{
throw new SonarrStartupException("Cannot create AppFolder, Access to the path {0} is denied", _appFolderInfo.AppDataFolder);
}
if (OsInfo.IsWindows)
{

View File

@@ -32,7 +32,19 @@ namespace NzbDrone.Common.Extensions
{
if (response == null || response.Content == null) return ex;
var contentSample = response.Content.Substring(0, Math.Min(response.Content.Length, 512));
var contentSample = response.Content.Substring(0, Math.Min(response.Content.Length, maxSampleLength));
if (response.Request != null)
{
ex.AddData("RequestUri", response.Request.Url.ToString());
if (response.Request.ContentSummary != null)
{
ex.AddData("RequestSummary", response.Request.ContentSummary);
}
}
ex.AddData("StatusCode", response.StatusCode.ToString());
if (response.Headers != null)
{

View File

@@ -51,6 +51,34 @@ namespace NzbDrone.Common.Extensions
}
}
public static Dictionary<TKey, TItem> ToDictionaryIgnoreDuplicates<TItem, TKey>(this IEnumerable<TItem> src, Func<TItem, TKey> keySelector)
{
var result = new Dictionary<TKey, TItem>();
foreach (var item in src)
{
var key = keySelector(item);
if (!result.ContainsKey(key))
{
result[key] = item;
}
}
return result;
}
public static Dictionary<TKey, TValue> ToDictionaryIgnoreDuplicates<TItem, TKey, TValue>(this IEnumerable<TItem> src, Func<TItem, TKey> keySelector, Func<TItem, TValue> valueSelector)
{
var result = new Dictionary<TKey, TValue>();
foreach (var item in src)
{
var key = keySelector(item);
if (!result.ContainsKey(key))
{
result[key] = valueSelector(item);
}
}
return result;
}
public static void AddIfNotNull<TSource>(this List<TSource> source, TSource item)
{
if (item == null)
@@ -81,4 +109,4 @@ namespace NzbDrone.Common.Extensions
return source.Select(predicate).ToList();
}
}
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Globalization;
namespace NzbDrone.Common.Extensions
{
@@ -6,7 +7,7 @@ namespace NzbDrone.Common.Extensions
{
public static int? ParseInt32(this string source)
{
int result = 0;
int result;
if (int.TryParse(source, out result))
{
@@ -16,9 +17,9 @@ namespace NzbDrone.Common.Extensions
return null;
}
public static Nullable<long> ParseInt64(this string source)
public static long? ParseInt64(this string source)
{
long result = 0;
long result;
if (long.TryParse(source, out result))
{
@@ -27,5 +28,17 @@ namespace NzbDrone.Common.Extensions
return null;
}
public static double? ParseDouble(this string source)
{
double result;
if (double.TryParse(source.Replace(',', '.'), NumberStyles.Number, CultureInfo.InvariantCulture, out result))
{
return result;
}
return null;
}
}
}
}

View File

@@ -52,7 +52,9 @@ namespace NzbDrone.Common.Http
public HttpResponse Execute(HttpRequest request)
{
var response = ExecuteRequest(request);
var cookieContainer = InitializeRequestCookies(request);
var response = ExecuteRequest(request, cookieContainer);
if (request.AllowAutoRedirect && response.HasHttpRedirect)
{
@@ -71,7 +73,7 @@ namespace NzbDrone.Common.Http
throw new WebException($"Too many automatic redirections were attempted for {autoRedirectChain.Join(" -> ")}", WebExceptionStatus.ProtocolError);
}
response = ExecuteRequest(request);
response = ExecuteRequest(request, cookieContainer);
}
while (response.HasHttpRedirect);
}
@@ -98,7 +100,7 @@ namespace NzbDrone.Common.Http
return response;
}
private HttpResponse ExecuteRequest(HttpRequest request)
private HttpResponse ExecuteRequest(HttpRequest request, CookieContainer cookieContainer)
{
foreach (var interceptor in _requestInterceptors)
{
@@ -114,11 +116,11 @@ namespace NzbDrone.Common.Http
var stopWatch = Stopwatch.StartNew();
var cookies = PrepareRequestCookies(request);
PrepareRequestCookies(request, cookieContainer);
var response = _httpDispatcher.GetResponse(request, cookies);
var response = _httpDispatcher.GetResponse(request, cookieContainer);
HandleResponseCookies(request, cookies);
HandleResponseCookies(response, cookieContainer);
stopWatch.Stop();
@@ -137,49 +139,91 @@ namespace NzbDrone.Common.Http
return response;
}
private CookieContainer PrepareRequestCookies(HttpRequest request)
private CookieContainer InitializeRequestCookies(HttpRequest request)
{
lock (_cookieContainerCache)
{
var persistentCookieContainer = _cookieContainerCache.Get("container", () => new CookieContainer());
var sourceContainer = new CookieContainer();
var presistentContainer = _cookieContainerCache.Get("container", () => new CookieContainer());
var persistentCookies = presistentContainer.GetCookies((Uri)request.Url);
sourceContainer.Add(persistentCookies);
if (request.Cookies.Count != 0)
{
foreach (var pair in request.Cookies)
{
persistentCookieContainer.Add(new Cookie(pair.Key, pair.Value, "/", request.Url.Host)
Cookie cookie;
if (pair.Value == null)
{
// Use Now rather than UtcNow to work around Mono cookie expiry bug.
// See https://gist.github.com/ta264/7822b1424f72e5b4c961
Expires = DateTime.Now.AddHours(1)
});
cookie = new Cookie(pair.Key, "", "/")
{
Expires = DateTime.Now.AddDays(-1)
};
}
else
{
cookie = new Cookie(pair.Key, pair.Value, "/")
{
// Use Now rather than UtcNow to work around Mono cookie expiry bug.
// See https://gist.github.com/ta264/7822b1424f72e5b4c961
Expires = DateTime.Now.AddHours(1)
};
}
sourceContainer.Add((Uri)request.Url, cookie);
if (request.StoreRequestCookie)
{
presistentContainer.Add((Uri)request.Url, cookie);
}
}
}
var requestCookies = persistentCookieContainer.GetCookies((Uri)request.Url);
var cookieContainer = new CookieContainer();
cookieContainer.Add(requestCookies);
return cookieContainer;
return sourceContainer;
}
}
private void HandleResponseCookies(HttpRequest request, CookieContainer cookieContainer)
private void PrepareRequestCookies(HttpRequest request, CookieContainer cookieContainer)
{
if (!request.StoreResponseCookie)
// Don't collect persistnet cookies for intermediate/redirected urls.
/*lock (_cookieContainerCache)
{
var presistentContainer = _cookieContainerCache.Get("container", () => new CookieContainer());
var persistentCookies = presistentContainer.GetCookies((Uri)request.Url);
var existingCookies = cookieContainer.GetCookies((Uri)request.Url);
cookieContainer.Add(persistentCookies);
cookieContainer.Add(existingCookies);
}*/
}
private void HandleResponseCookies(HttpResponse response, CookieContainer cookieContainer)
{
var cookieHeaders = response.GetCookieHeaders();
if (cookieHeaders.Empty())
{
return;
}
lock (_cookieContainerCache)
if (response.Request.StoreResponseCookie)
{
var persistentCookieContainer = _cookieContainerCache.Get("container", () => new CookieContainer());
lock (_cookieContainerCache)
{
var persistentCookieContainer = _cookieContainerCache.Get("container", () => new CookieContainer());
var cookies = cookieContainer.GetCookies((Uri)request.Url);
persistentCookieContainer.Add(cookies);
foreach (var cookieHeader in cookieHeaders)
{
try
{
persistentCookieContainer.SetCookies((Uri)response.Request.Url, cookieHeader);
}
catch (Exception ex)
{
_logger.Debug(ex, "Invalid cookie in {0}", response.Request.Url);
}
}
}
}
}

View File

@@ -13,8 +13,10 @@ namespace NzbDrone.Common.Http
Url = new HttpUri(url);
Headers = new HttpHeader();
AllowAutoRedirect = true;
StoreRequestCookie = true;
Cookies = new Dictionary<string, string>();
if (!RuntimeInfo.IsProduction)
{
AllowAutoRedirect = false;
@@ -37,6 +39,7 @@ namespace NzbDrone.Common.Http
public bool ConnectionKeepAlive { get; set; }
public bool LogResponseContent { get; set; }
public Dictionary<string, string> Cookies { get; private set; }
public bool StoreRequestCookie { get; set; }
public bool StoreResponseCookie { get; set; }
public TimeSpan RequestTimeout { get; set; }
public TimeSpan RateLimit { get; set; }

View File

@@ -55,20 +55,22 @@ namespace NzbDrone.Common.Http
StatusCode == HttpStatusCode.MovedPermanently ||
StatusCode == HttpStatusCode.Found;
public string[] GetCookieHeaders()
{
return Headers.GetValues("Set-Cookie") ?? new string[0];
}
public Dictionary<string, string> GetCookies()
{
var result = new Dictionary<string, string>();
var setCookieHeaders = Headers.GetValues("Set-Cookie");
if (setCookieHeaders != null)
var setCookieHeaders = GetCookieHeaders();
foreach (var cookie in setCookieHeaders)
{
foreach (var cookie in setCookieHeaders)
var match = RegexSetCookie.Match(cookie);
if (match.Success)
{
var match = RegexSetCookie.Match(cookie);
if (match.Success)
{
result[match.Groups[1].Value] = match.Groups[2].Value;
}
result[match.Groups[1].Value] = match.Groups[2].Value;
}
}

View File

@@ -44,7 +44,7 @@
<Private>True</Private>
</Reference>
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<HintPath>..\packages\NLog.4.5.0-rc06\lib\net40-client\NLog.dll</HintPath>
<HintPath>..\packages\NLog.4.5.3\lib\net40-client\NLog.dll</HintPath>
</Reference>
<Reference Include="Org.Mentalis, Version=1.0.0.1, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\DotNet4.SocksProxy.1.3.4.0\lib\net40\Org.Mentalis.dll</HintPath>
@@ -60,11 +60,14 @@
<Reference Include="System.Configuration.Install" />
<Reference Include="System.Core" />
<Reference Include="System.Data" />
<Reference Include="System.Runtime.Serialization" />
<Reference Include="System.ServiceModel" />
<Reference Include="System.ServiceProcess" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="ICSharpCode.SharpZipLib">
<HintPath>..\packages\ICSharpCode.SharpZipLib.Patched.0.86.5\lib\net20\ICSharpCode.SharpZipLib.dll</HintPath>
</Reference>
<Reference Include="System.Transactions" />
<Reference Include="System.Xml" />
<Reference Include="System.Xml.Linq" />
</ItemGroup>

View File

@@ -3,6 +3,6 @@
<package id="DotNet4.SocksProxy" version="1.3.4.0" targetFramework="net40" />
<package id="ICSharpCode.SharpZipLib.Patched" version="0.86.5" targetFramework="net40" />
<package id="Newtonsoft.Json" version="9.0.1" targetFramework="net40" />
<package id="NLog" version="4.5.0-rc06" targetFramework="net40" />
<package id="NLog" version="4.5.3" targetFramework="net40" />
<package id="SharpRaven" version="2.2.0" targetFramework="net40" />
</packages>

View File

@@ -66,6 +66,7 @@
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.CSharp" />
<Reference Include="Microsoft.Owin, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\Microsoft.Owin.2.1.0\lib\net40\Microsoft.Owin.dll</HintPath>
@@ -79,13 +80,19 @@
<Private>True</Private>
</Reference>
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<HintPath>..\packages\NLog.4.5.0-rc06\lib\net40-client\NLog.dll</HintPath>
<HintPath>..\packages\NLog.4.5.3\lib\net40-client\NLog.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Configuration" />
<Reference Include="System.Core" />
<Reference Include="Owin">
<HintPath>..\packages\Owin.1.0\lib\net40\Owin.dll</HintPath>
</Reference>
<Reference Include="System.Data" />
<Reference Include="System.Runtime.Serialization" />
<Reference Include="System.ServiceModel" />
<Reference Include="System.Transactions" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\NzbDrone.Common\Properties\SharedAssemblyInfo.cs">

View File

@@ -3,6 +3,6 @@
<package id="Microsoft.Owin" version="2.1.0" targetFramework="net40" />
<package id="Microsoft.Owin.Hosting" version="2.1.0" targetFramework="net40" />
<package id="Newtonsoft.Json" version="9.0.1" targetFramework="net40" />
<package id="NLog" version="4.5.0-rc06" targetFramework="net40" />
<package id="NLog" version="4.5.3" targetFramework="net40" />
<package id="Owin" version="1.0" targetFramework="net40" />
</packages>

View File

@@ -0,0 +1,41 @@
using System.Linq;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Datastore.Migration;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Datastore.Migration
{
[TestFixture]
public class update_animetosho_urlFixture : MigrationTest<update_animetosho_url>
{
[TestCase("Newznab", "https://animetosho.org")]
[TestCase("Newznab", "http://animetosho.org")]
[TestCase("Torznab", "https://animetosho.org")]
[TestCase("Torznab", "http://animetosho.org")]
public void should_replace_old_url(string impl, string baseUrl)
{
var db = WithMigrationTestDb(c =>
{
c.Insert.IntoTable("Indexers").Row(new
{
Name = "AnimeTosho",
Implementation = impl,
Settings = new NewznabSettings121
{
BaseUrl = baseUrl,
ApiPath = "/feed/nabapi"
}.ToJson(),
ConfigContract = impl + "Settings"
});
});
var items = db.Query<IndexerDefinition90>("SELECT * FROM Indexers");
items.Should().HaveCount(1);
items.First().Settings.ToObject<NewznabSettings121>().BaseUrl.Should().Be(baseUrl.Replace("animetosho", "feed.animetosho"));
}
}
}

View File

@@ -212,7 +212,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
decisions.Add(new DownloadDecision(remoteEpisode, new Rejection("Failure!", RejectionType.Temporary)));
Subject.ProcessDecisions(decisions);
Mocker.GetMock<IPendingReleaseService>().Verify(v => v.Add(It.IsAny<DownloadDecision>(), It.IsAny<PendingReleaseReason>()), Times.Never());
Mocker.GetMock<IPendingReleaseService>().Verify(v => v.AddMany(It.IsAny<List<Tuple<DownloadDecision, PendingReleaseReason>>>()), Times.Never());
}
[Test]
@@ -226,7 +226,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
decisions.Add(new DownloadDecision(remoteEpisode, new Rejection("Failure!", RejectionType.Temporary)));
Subject.ProcessDecisions(decisions);
Mocker.GetMock<IPendingReleaseService>().Verify(v => v.Add(It.IsAny<DownloadDecision>(), It.IsAny<PendingReleaseReason>()), Times.Exactly(2));
Mocker.GetMock<IPendingReleaseService>().Verify(v => v.AddMany(It.IsAny<List<Tuple<DownloadDecision, PendingReleaseReason>>>()), Times.Once());
}
[Test]

View File

@@ -73,6 +73,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
Transfer = new Dictionary<string, string>
{
{ "size_downloaded", "0"},
{ "size_uploaded", "0"},
{ "speed_download", "0" }
}
}
@@ -96,6 +97,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
Transfer = new Dictionary<string, string>
{
{ "size_downloaded", "1000"},
{ "size_uploaded", "100"},
{ "speed_download", "0" }
},
}
@@ -119,6 +121,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
Transfer = new Dictionary<string, string>
{
{ "size_downloaded", "1000"},
{ "size_uploaded", "100"},
{ "speed_download", "0" }
}
}
@@ -142,6 +145,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
Transfer = new Dictionary<string, string>
{
{ "size_downloaded", "100"},
{ "size_uploaded", "10"},
{ "speed_download", "50" }
}
}
@@ -165,6 +169,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
Transfer = new Dictionary<string, string>
{
{ "size_downloaded", "10"},
{ "size_uploaded", "1"},
{ "speed_download", "0" }
}
}
@@ -188,6 +193,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
Transfer = new Dictionary<string, string>
{
{ "size_downloaded", "1000"},
{ "size_uploaded", "100"},
{ "speed_download", "0" }
}
}
@@ -211,6 +217,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
Transfer = new Dictionary<string, string>
{
{ "size_downloaded", "1000"},
{ "size_uploaded", "100"},
{ "speed_download", "0" }
}
}
@@ -234,6 +241,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
Transfer = new Dictionary<string, string>
{
{ "size_downloaded", "1000"},
{ "size_uploaded", "100"},
{ "speed_download", "0" }
}
}
@@ -257,6 +265,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
Transfer = new Dictionary<string, string>
{
{ "size_downloaded", "1000"},
{ "size_uploaded", "100"},
{ "speed_download", "0" }
}
}

View File

@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using FizzWare.NBuilder;
using Marr.Data;
@@ -54,8 +54,10 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
_release = Builder<ReleaseInfo>.CreateNew().Build();
_parsedEpisodeInfo = Builder<ParsedEpisodeInfo>.CreateNew().Build();
_parsedEpisodeInfo.Quality = new QualityModel(Quality.HDTV720p);
_parsedEpisodeInfo = Builder<ParsedEpisodeInfo>.CreateNew()
.With(h => h.Quality = new QualityModel(Quality.HDTV720p))
.With(h => h.AirDate = null)
.Build();
_remoteEpisode = new RemoteEpisode();
_remoteEpisode.Episodes = new List<Episode>{ _episode };

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using FizzWare.NBuilder;
using Marr.Data;
@@ -98,6 +98,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
.With(h => h.SeriesId = _series.Id)
.With(h => h.Title = title)
.With(h => h.Release = release)
.With(h => h.ParsedEpisodeInfo = new ParsedEpisodeInfo())
.Build();
Mocker.GetMock<IPendingReleaseRepository>()

View File

@@ -8,7 +8,7 @@ using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.Metadata.Consumers.Roksbox
namespace NzbDrone.Core.Test.Extras.Metadata.Consumers.Roksbox
{
[TestFixture]
public class FindMetadataFileFixture : CoreTest<RoksboxMetadata>

View File

@@ -8,7 +8,7 @@ using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.Metadata.Consumers.Wdtv
namespace NzbDrone.Core.Test.Extras.Metadata.Consumers.Wdtv
{
[TestFixture]
public class FindMetadataFileFixture : CoreTest<WdtvMetadata>

View File

@@ -0,0 +1,65 @@
using System.IO;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.Extras.Metadata;
using NzbDrone.Core.Extras.Metadata.Consumers.Xbmc;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.Extras.Metadata.Consumers.Xbmc
{
[TestFixture]
public class FindMetadataFileFixture : CoreTest<XbmcMetadata>
{
private Series _series;
[SetUp]
public void Setup()
{
_series = Builder<Series>.CreateNew()
.With(s => s.Path = @"C:\Test\TV\The.Series".AsOsAgnostic())
.Build();
}
[Test]
public void should_return_null_if_filename_is_not_handled()
{
var path = Path.Combine(_series.Path, "file.jpg");
Subject.FindMetadataFile(_series, path).Should().BeNull();
}
[Test]
public void should_return_metadata_for_xbmc_nfo()
{
var path = Path.Combine(_series.Path, "the.series.s01e01.episode.nfo");
Mocker.GetMock<IDetectXbmcNfo>()
.Setup(v => v.IsXbmcNfoFile(path))
.Returns(true);
Subject.FindMetadataFile(_series, path).Type.Should().Be(MetadataType.EpisodeMetadata);
Mocker.GetMock<IDetectXbmcNfo>()
.Verify(v => v.IsXbmcNfoFile(It.IsAny<string>()), Times.Once());
}
[Test]
public void should_return_null_for_scene_nfo()
{
var path = Path.Combine(_series.Path, "the.series.s01e01.episode.nfo");
Mocker.GetMock<IDetectXbmcNfo>()
.Setup(v => v.IsXbmcNfoFile(path))
.Returns(false);
Subject.FindMetadataFile(_series, path).Should().BeNull();
Mocker.GetMock<IDetectXbmcNfo>()
.Verify(v => v.IsXbmcNfoFile(It.IsAny<string>()), Times.Once());
}
}
}

View File

@@ -0,0 +1,59 @@
using NUnit.Framework;
using NzbDrone.Core.HealthCheck.Checks;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common;
using static NzbDrone.Core.HealthCheck.Checks.MonoDebugCheck;
namespace NzbDrone.Core.Test.HealthCheck.Checks
{
[TestFixture]
public class MonoDebugFixture : CoreTest<MonoDebugCheck>
{
private void GivenHasStackFrame(bool hasStackFrame)
{
Mocker.GetMock<StackFrameHelper>()
.Setup(f => f.HasStackFrameInfo())
.Returns(hasStackFrame);
}
[Test]
public void should_return_ok_if_windows()
{
WindowsOnly();
Subject.Check().ShouldBeOk();
}
[Test]
public void should_return_ok_if_not_debug()
{
MonoOnly();
GivenHasStackFrame(false);
Subject.Check().ShouldBeOk();
}
[Test]
public void should_log_warning_if_not_debug()
{
MonoOnly();
GivenHasStackFrame(false);
Subject.Check();
ExceptionVerification.ExpectedWarns(1);
}
[Test]
public void should_return_ok_if_debug()
{
MonoOnly();
GivenHasStackFrame(true);
Subject.Check().ShouldBeOk();
}
}
}

View File

@@ -1,16 +1,17 @@
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.DataAugmentation.Scene;
using NzbDrone.Core.IndexerSearch;
using NzbDrone.Core.Test.Framework;
using FizzWare.NBuilder;
using System;
using System.Collections.Generic;
using System.Linq;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.Tv;
using NzbDrone.Core.DataAugmentation.Scene;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.IndexerSearch;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Test.IndexerSearchTests
{
@@ -55,7 +56,7 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
.Returns(new List<string>());
}
private void WithEpisode(int seasonNumber, int episodeNumber, int? sceneSeasonNumber, int? sceneEpisodeNumber)
private void WithEpisode(int seasonNumber, int episodeNumber, int? sceneSeasonNumber, int? sceneEpisodeNumber, string airDate = null)
{
var episode = Builder<Episode>.CreateNew()
.With(v => v.SeriesId == _xemSeries.Id)
@@ -64,6 +65,7 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
.With(v => v.EpisodeNumber, episodeNumber)
.With(v => v.SceneSeasonNumber, sceneSeasonNumber)
.With(v => v.SceneEpisodeNumber, sceneEpisodeNumber)
.With(v => v.AirDate = (airDate ?? $"{2000 + seasonNumber}-{episodeNumber:00}-05"))
.With(v => v.Monitored = true)
.Build();
@@ -108,10 +110,22 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
.Callback<SeasonSearchCriteria>(s => result.Add(s))
.Returns(new List<Parser.Model.ReleaseInfo>());
_mockIndexer.Setup(v => v.Fetch(It.IsAny<DailyEpisodeSearchCriteria>()))
.Callback<DailyEpisodeSearchCriteria>(s => result.Add(s))
.Returns(new List<Parser.Model.ReleaseInfo>());
_mockIndexer.Setup(v => v.Fetch(It.IsAny<DailySeasonSearchCriteria>()))
.Callback<DailySeasonSearchCriteria>(s => result.Add(s))
.Returns(new List<Parser.Model.ReleaseInfo>());
_mockIndexer.Setup(v => v.Fetch(It.IsAny<AnimeEpisodeSearchCriteria>()))
.Callback<AnimeEpisodeSearchCriteria>(s => result.Add(s))
.Returns(new List<Parser.Model.ReleaseInfo>());
_mockIndexer.Setup(v => v.Fetch(It.IsAny<SpecialEpisodeSearchCriteria>()))
.Callback<SpecialEpisodeSearchCriteria>(s => result.Add(s))
.Returns(new List<Parser.Model.ReleaseInfo>());
return result;
}
@@ -249,6 +263,68 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
criteria.Count.Should().Be(0);
}
[Test]
public void season_search_for_daily_should_search_multiple_years()
{
WithEpisode(1, 1, null, null, "2005-12-30");
WithEpisode(1, 2, null, null, "2005-12-31");
WithEpisode(1, 3, null, null, "2006-01-01");
WithEpisode(1, 4, null, null, "2006-01-02");
_xemSeries.SeriesType = SeriesTypes.Daily;
var allCriteria = WatchForSearchCriteria();
Subject.SeasonSearch(_xemSeries.Id, 1, false, true);
var criteria = allCriteria.OfType<DailySeasonSearchCriteria>().ToList();
criteria.Count.Should().Be(2);
criteria[0].Year.Should().Be(2005);
criteria[1].Year.Should().Be(2006);
}
[Test]
public void season_search_for_daily_should_search_single_episode_if_possible()
{
WithEpisode(1, 1, null, null, "2005-12-30");
WithEpisode(1, 2, null, null, "2005-12-31");
WithEpisode(1, 3, null, null, "2006-01-01");
_xemSeries.SeriesType = SeriesTypes.Daily;
var allCriteria = WatchForSearchCriteria();
Subject.SeasonSearch(_xemSeries.Id, 1, false, true);
var criteria1 = allCriteria.OfType<DailySeasonSearchCriteria>().ToList();
var criteria2 = allCriteria.OfType<DailyEpisodeSearchCriteria>().ToList();
criteria1.Count.Should().Be(1);
criteria1[0].Year.Should().Be(2005);
criteria2.Count.Should().Be(1);
criteria2[0].AirDate.Should().Be(new DateTime(2006, 1, 1));
}
[Test]
public void season_search_for_daily_should_not_search_for_unmonitored_episodes()
{
WithEpisode(1, 1, null, null, "2005-12-30");
WithEpisode(1, 2, null, null, "2005-12-31");
WithEpisode(1, 3, null, null, "2006-01-01");
_xemSeries.SeriesType = SeriesTypes.Daily;
_xemEpisodes[0].Monitored = false;
var allCriteria = WatchForSearchCriteria();
Subject.SeasonSearch(_xemSeries.Id, 1, false, true);
var criteria1 = allCriteria.OfType<DailySeasonSearchCriteria>().ToList();
var criteria2 = allCriteria.OfType<DailyEpisodeSearchCriteria>().ToList();
criteria1.Should().HaveCount(0);
criteria2.Should().HaveCount(2);
}
[Test]
public void getscenenames_should_use_seasonnumber_if_no_scene_seasonnumber_is_available()
{

View File

@@ -0,0 +1,65 @@
using System;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.Torznab;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.IndexerTests
{
[TestFixture]
public class SeedConfigProviderFixture : CoreTest<SeedConfigProvider>
{
[Test]
public void should_not_return_config_for_non_existent_indexer()
{
Mocker.GetMock<IIndexerFactory>()
.Setup(v => v.Get(It.IsAny<int>()))
.Throws(new ModelNotFoundException(typeof(IndexerDefinition), 0));
var result = Subject.GetSeedConfiguration(new RemoteEpisode
{
Release = new ReleaseInfo()
{
DownloadProtocol = DownloadProtocol.Torrent,
IndexerId = 0
}
});
result.Should().BeNull();
}
[Test]
public void should_return_season_time_for_season_packs()
{
var settings = new TorznabSettings();
settings.SeedCriteria.SeasonPackSeedTime = 10;
Mocker.GetMock<IIndexerFactory>()
.Setup(v => v.Get(It.IsAny<int>()))
.Returns(new IndexerDefinition
{
Settings = settings
});
var result = Subject.GetSeedConfiguration(new RemoteEpisode
{
Release = new ReleaseInfo()
{
DownloadProtocol = DownloadProtocol.Torrent,
IndexerId = 1
},
ParsedEpisodeInfo = new ParsedEpisodeInfo
{
FullSeason = true
}
});
result.Should().NotBeNull();
result.SeedTime.Should().Be(TimeSpan.FromMinutes(10));
}
}
}

View File

@@ -0,0 +1,109 @@
using System.Collections.Generic;
using System.Linq;
using FizzWare.NBuilder;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators
{
[TestFixture]
public class AugmentEpisodesFixture : CoreTest<AggregateEpisodes>
{
private Series _series;
[SetUp]
public void Setup()
{
_series = Builder<Series>.CreateNew().Build();
var augmenters = new List<Mock<IAggregateLocalEpisode>>
{
new Mock<IAggregateLocalEpisode>()
};
Mocker.SetConstant(augmenters.Select(c => c.Object));
}
[Test]
public void should_not_use_folder_for_full_season()
{
var fileEpisodeInfo = Parser.Parser.ParseTitle("Series.Title.S01E01");
var folderEpisodeInfo = Parser.Parser.ParseTitle("Series.Title.S01");
var localEpisode = new LocalEpisode
{
FileEpisodeInfo = fileEpisodeInfo,
FolderEpisodeInfo = folderEpisodeInfo,
Path = @"C:\Test\Unsorted TV\Series.Title.S01\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_not_use_folder_when_it_contains_more_than_one_valid_video_file()
{
var fileEpisodeInfo = Parser.Parser.ParseTitle("Series.Title.S01E01");
var folderEpisodeInfo = Parser.Parser.ParseTitle("Series.Title.S01");
var localEpisode = new LocalEpisode
{
FileEpisodeInfo = fileEpisodeInfo,
FolderEpisodeInfo = folderEpisodeInfo,
Path = @"C:\Test\Unsorted TV\Series.Title.S01\Series.Title.S01E01.mkv".AsOsAgnostic(),
Series = _series
};
Subject.Aggregate(localEpisode, true);
Mocker.GetMock<IParsingService>()
.Verify(v => v.GetEpisodes(fileEpisodeInfo, _series, localEpisode.SceneSource, null), Times.Once());
}
[Test]
public void should_not_use_folder_name_if_file_name_is_scene_name()
{
var fileEpisodeInfo = Parser.Parser.ParseTitle("Series.Title.S01E01");
var folderEpisodeInfo = Parser.Parser.ParseTitle("Series.Title.S01E01");
var localEpisode = new LocalEpisode
{
FileEpisodeInfo = fileEpisodeInfo,
FolderEpisodeInfo = folderEpisodeInfo,
Path = @"C:\Test\Unsorted TV\Series.Title.S01E01\Series.Title.S01E01.720p.HDTV-Sonarr.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_folder_when_only_one_video_file()
{
var fileEpisodeInfo = Parser.Parser.ParseTitle("Series.Title.S01E01");
var folderEpisodeInfo = Parser.Parser.ParseTitle("Series.Title.S01E01");
var localEpisode = new LocalEpisode
{
FileEpisodeInfo = fileEpisodeInfo,
FolderEpisodeInfo = folderEpisodeInfo,
Path = @"C:\Test\Unsorted TV\Series.Title.S01E01\Series.Title.S01E01.mkv".AsOsAgnostic(),
Series = _series
};
Subject.Aggregate(localEpisode, false);
Mocker.GetMock<IParsingService>()
.Verify(v => v.GetEpisodes(folderEpisodeInfo, _series, localEpisode.SceneSource, null), Times.Once());
}
}
}

View File

@@ -0,0 +1,93 @@
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators;
using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators.Augmenters.Quality;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators
{
[TestFixture]
public class AugmentQualityFixture : CoreTest<AggregateQuality>
{
private Mock<IAugmentQuality> _mediaInfoAugmenter;
private Mock<IAugmentQuality> _fileExtensionAugmenter;
private Mock<IAugmentQuality> _nameAugmenter;
private IEnumerable<IAugmentQuality> _qualityAugmenters;
[SetUp]
public void Setup()
{
_mediaInfoAugmenter = new Mock<IAugmentQuality>();
_fileExtensionAugmenter = new Mock<IAugmentQuality>();
_nameAugmenter = new Mock<IAugmentQuality>();
_mediaInfoAugmenter.Setup(s => s.AugmentQuality(It.IsAny<LocalEpisode>()))
.Returns(AugmentQualityResult.ResolutionOnly(1080, Confidence.MediaInfo));
_fileExtensionAugmenter.Setup(s => s.AugmentQuality(It.IsAny<LocalEpisode>()))
.Returns(new AugmentQualityResult(QualitySource.Television, Confidence.Fallback, 720, Confidence.Fallback, new Revision()));
_nameAugmenter.Setup(s => s.AugmentQuality(It.IsAny<LocalEpisode>()))
.Returns(new AugmentQualityResult(QualitySource.Television, Confidence.Default, 480, Confidence.Default, new Revision()));
}
private void GivenAugmenters(params Mock<IAugmentQuality>[] mocks)
{
Mocker.SetConstant<IEnumerable<IAugmentQuality>>(mocks.Select(c => c.Object));
}
[Test]
public void should_return_HDTV720_from_extension_when_other_augments_are_null()
{
var nullMock = new Mock<IAugmentQuality>();
nullMock.Setup(s => s.AugmentQuality(It.IsAny<LocalEpisode>()))
.Returns<LocalEpisode>(l => null);
GivenAugmenters(_fileExtensionAugmenter, nullMock);
var result = Subject.Aggregate(new LocalEpisode(), false);
result.Quality.QualityDetectionSource.Should().Be(QualityDetectionSource.Extension);
result.Quality.Quality.Should().Be(Quality.HDTV720p);
}
[Test]
public void should_return_SDTV_when_HDTV720_came_from_extension()
{
GivenAugmenters(_fileExtensionAugmenter, _nameAugmenter);
var result = Subject.Aggregate(new LocalEpisode(), false);
result.Quality.QualityDetectionSource.Should().Be(QualityDetectionSource.Name);
result.Quality.Quality.Should().Be(Quality.SDTV);
}
[Test]
public void should_return_HDTV1080p_when_HDTV720_came_from_extension_and_mediainfo_indicates_1080()
{
GivenAugmenters(_fileExtensionAugmenter, _mediaInfoAugmenter);
var result = Subject.Aggregate(new LocalEpisode(), false);
result.Quality.QualityDetectionSource.Should().Be(QualityDetectionSource.MediaInfo);
result.Quality.Quality.Should().Be(Quality.HDTV1080p);
}
[Test]
public void should_return_HDTV1080p_when_SDTV_came_from_name_and_mediainfo_indicates_1080()
{
GivenAugmenters(_nameAugmenter, _mediaInfoAugmenter);
var result = Subject.Aggregate(new LocalEpisode(), false);
result.Quality.QualityDetectionSource.Should().Be(QualityDetectionSource.MediaInfo);
result.Quality.Quality.Should().Be(Quality.HDTV1080p);
}
}
}

View File

@@ -0,0 +1,68 @@
using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators.Augmenters.Quality;
using NzbDrone.Core.MediaFiles.MediaInfo;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators.Augmenters.Quality
{
[TestFixture]
public class AugmentQualityFromMediaInfoFixture : CoreTest<AugmentQualityFromMediaInfo>
{
[Test]
public void should_return_null_if_media_info_is_null()
{
var localEpisode = Builder<LocalEpisode>.CreateNew()
.With(l => l.MediaInfo = null)
.Build();
Subject.AugmentQuality(localEpisode).Should().Be(null);
}
[Test]
public void should_return_null_if_media_info_width_is_zero()
{
var mediaInfo = Builder<MediaInfoModel>.CreateNew()
.With(m => m.Width = 0)
.Build();
var localEpisode = Builder<LocalEpisode>.CreateNew()
.With(l => l.MediaInfo = mediaInfo)
.Build();
Subject.AugmentQuality(localEpisode).Should().Be(null);
}
[TestCase(4096, 2160)] // True 4K
[TestCase(4000, 2160)]
[TestCase(3840, 2160)] // 4K UHD
[TestCase(3200, 2160)]
[TestCase(2000, 1080)]
[TestCase(1920, 1080)] // Full HD
[TestCase(1800, 1080)]
[TestCase(1490, 720)]
[TestCase(1280, 720)] // HD
[TestCase(1200, 720)]
[TestCase(800, 480)]
[TestCase(720, 480)] // SDTV
[TestCase(600, 480)]
[TestCase(100, 480)]
public void should_return_closest_resolution(int mediaInfoWidth, int expectedResolution)
{
var mediaInfo = Builder<MediaInfoModel>.CreateNew()
.With(m => m.Width = mediaInfoWidth)
.Build();
var localEpisode = Builder<LocalEpisode>.CreateNew()
.With(l => l.MediaInfo = mediaInfo)
.Build();
var result = Subject.AugmentQuality(localEpisode);
result.Should().NotBe(null);
result.Resolution.Should().Be(expectedResolution);
}
}
}

View File

@@ -6,7 +6,6 @@ using NUnit.Framework;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.EpisodeImport;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Profiles;
using NzbDrone.Core.Qualities;
@@ -15,6 +14,7 @@ using NzbDrone.Core.Tv;
using NzbDrone.Test.Common;
using FizzWare.NBuilder;
using NzbDrone.Core.Download;
using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation;
namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
{
@@ -54,6 +54,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
_fail3.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalEpisode>(), It.IsAny<DownloadClientItem>())).Returns(Decision.Reject("_fail3"));
_series = Builder<Series>.CreateNew()
.With(e => e.Path = @"C:\Test\Series".AsOsAgnostic())
.With(e => e.Profile = new Profile { Items = Qualities.QualityFixture.GetDefaultQualities() })
.Build();
@@ -67,10 +68,6 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
Path = @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV.avi"
};
Mocker.GetMock<IParsingService>()
.Setup(c => c.GetLocalEpisode(It.IsAny<string>(), It.IsAny<Series>(), It.IsAny<ParsedEpisodeInfo>(), It.IsAny<bool>()))
.Returns(_localEpisode);
GivenVideoFiles(new List<string> { @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV.avi".AsOsAgnostic() });
}
@@ -88,20 +85,31 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
.Returns(_videoFiles);
}
private void GivenAugmentationSuccess()
{
Mocker.GetMock<IAugmentingService>()
.Setup(s => s.Augment(It.IsAny<LocalEpisode>(), It.IsAny<bool>()))
.Callback<LocalEpisode, bool>((localEpisode, otherFiles) =>
{
localEpisode.Episodes = _localEpisode.Episodes;
});
}
[Test]
public void should_call_all_specifications()
{
var downloadClientItem = Builder<DownloadClientItem>.CreateNew().Build();
GivenAugmentationSuccess();
GivenSpecifications(_pass1, _pass2, _pass3, _fail1, _fail2, _fail3);
Subject.GetImportDecisions(_videoFiles, new Series(), downloadClientItem, null, false);
Subject.GetImportDecisions(_videoFiles, _series, downloadClientItem, null, false);
_fail1.Verify(c => c.IsSatisfiedBy(_localEpisode, downloadClientItem), Times.Once());
_fail2.Verify(c => c.IsSatisfiedBy(_localEpisode, downloadClientItem), Times.Once());
_fail3.Verify(c => c.IsSatisfiedBy(_localEpisode, downloadClientItem), Times.Once());
_pass1.Verify(c => c.IsSatisfiedBy(_localEpisode, downloadClientItem), Times.Once());
_pass2.Verify(c => c.IsSatisfiedBy(_localEpisode, downloadClientItem), Times.Once());
_pass3.Verify(c => c.IsSatisfiedBy(_localEpisode, downloadClientItem), Times.Once());
_fail1.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalEpisode>(), downloadClientItem), Times.Once());
_fail2.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalEpisode>(), downloadClientItem), Times.Once());
_fail3.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalEpisode>(), downloadClientItem), Times.Once());
_pass1.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalEpisode>(), downloadClientItem), Times.Once());
_pass2.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalEpisode>(), downloadClientItem), Times.Once());
_pass3.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalEpisode>(), downloadClientItem), Times.Once());
}
[Test]
@@ -109,7 +117,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
{
GivenSpecifications(_fail1);
var result = Subject.GetImportDecisions(_videoFiles, new Series());
var result = Subject.GetImportDecisions(_videoFiles, _series);
result.Single().Approved.Should().BeFalse();
}
@@ -119,17 +127,18 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
{
GivenSpecifications(_pass1, _fail1, _pass2, _pass3);
var result = Subject.GetImportDecisions(_videoFiles, new Series());
var result = Subject.GetImportDecisions(_videoFiles, _series);
result.Single().Approved.Should().BeFalse();
}
[Test]
public void should_return_pass_if_all_specs_pass()
public void should_return_approved_if_all_specs_pass()
{
GivenAugmentationSuccess();
GivenSpecifications(_pass1, _pass2, _pass3);
var result = Subject.GetImportDecisions(_videoFiles, new Series());
var result = Subject.GetImportDecisions(_videoFiles, _series);
result.Single().Approved.Should().BeTrue();
}
@@ -137,9 +146,10 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
[Test]
public void should_have_same_number_of_rejections_as_specs_that_failed()
{
GivenAugmentationSuccess();
GivenSpecifications(_pass1, _pass2, _pass3, _fail1, _fail2, _fail3);
var result = Subject.GetImportDecisions(_videoFiles, new Series());
var result = Subject.GetImportDecisions(_videoFiles, _series);
result.Single().Rejections.Should().HaveCount(3);
}
@@ -148,8 +158,8 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
{
GivenSpecifications(_pass1);
Mocker.GetMock<IParsingService>()
.Setup(c => c.GetLocalEpisode(It.IsAny<string>(), It.IsAny<Series>(), It.IsAny<ParsedEpisodeInfo>(), It.IsAny<bool>()))
Mocker.GetMock<IAugmentingService>()
.Setup(c => c.Augment(It.IsAny<LocalEpisode>(), It.IsAny<bool>()))
.Throws<TestException>();
_videoFiles = new List<string>
@@ -163,76 +173,17 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
Subject.GetImportDecisions(_videoFiles, _series);
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalEpisode(It.IsAny<string>(), It.IsAny<Series>(), It.IsAny<ParsedEpisodeInfo>(), It.IsAny<bool>()), Times.Exactly(_videoFiles.Count));
Mocker.GetMock<IAugmentingService>()
.Verify(c => c.Augment(It.IsAny<LocalEpisode>(), It.IsAny<bool>()), Times.Exactly(_videoFiles.Count));
ExceptionVerification.ExpectedErrors(3);
}
[Test]
public void should_use_file_quality_if_folder_quality_is_null()
{
GivenSpecifications(_pass1, _pass2, _pass3);
var expectedQuality = QualityParser.ParseQuality(_videoFiles.Single());
var result = Subject.GetImportDecisions(_videoFiles, _series);
result.Single().LocalEpisode.Quality.Should().Be(expectedQuality);
}
[Test]
public void should_use_file_quality_if_file_quality_was_determined_by_name()
{
GivenSpecifications(_pass1, _pass2, _pass3);
var expectedQuality = QualityParser.ParseQuality(_videoFiles.Single());
var result = Subject.GetImportDecisions(_videoFiles, _series, null, new ParsedEpisodeInfo{Quality = new QualityModel(Quality.SDTV)}, true);
result.Single().LocalEpisode.Quality.Should().Be(expectedQuality);
}
[Test]
public void should_use_folder_quality_when_file_quality_was_determined_by_the_extension()
{
GivenSpecifications(_pass1, _pass2, _pass3);
GivenVideoFiles(new string[] { @"C:\Test\Unsorted\The.Office.S03E115.mkv".AsOsAgnostic() });
_localEpisode.Path = _videoFiles.Single();
_localEpisode.Quality.QualitySource = QualitySource.Extension;
_localEpisode.Quality.Quality = Quality.HDTV720p;
var expectedQuality = new QualityModel(Quality.SDTV);
var result = Subject.GetImportDecisions(_videoFiles, _series, null, new ParsedEpisodeInfo { Quality = expectedQuality }, true);
result.Single().LocalEpisode.Quality.Should().Be(expectedQuality);
}
[Test]
public void should_use_folder_quality_when_greater_than_file_quality()
{
GivenSpecifications(_pass1, _pass2, _pass3);
GivenVideoFiles(new string[] { @"C:\Test\Unsorted\The.Office.S03E115.mkv".AsOsAgnostic() });
_localEpisode.Path = _videoFiles.Single();
_localEpisode.Quality.Quality = Quality.HDTV720p;
var expectedQuality = new QualityModel(Quality.Bluray720p);
var result = Subject.GetImportDecisions(_videoFiles, _series, null, new ParsedEpisodeInfo { Quality = expectedQuality }, true);
result.Single().LocalEpisode.Quality.Should().Be(expectedQuality);
}
[Test]
public void should_not_throw_if_episodes_are_not_found()
{
GivenSpecifications(_pass1);
Mocker.GetMock<IParsingService>()
.Setup(c => c.GetLocalEpisode(It.IsAny<string>(), It.IsAny<Series>(), It.IsAny<ParsedEpisodeInfo>(), It.IsAny<bool>()))
.Returns(new LocalEpisode() { Path = "test", ParsedEpisodeInfo = new ParsedEpisodeInfo { } });
_videoFiles = new List<string>
{
"The.Office.S03E115.DVDRip.XviD-OSiTV",
@@ -244,162 +195,18 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
var decisions = Subject.GetImportDecisions(_videoFiles, _series);
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalEpisode(It.IsAny<string>(), It.IsAny<Series>(), It.IsAny<ParsedEpisodeInfo>(), It.IsAny<bool>()), Times.Exactly(_videoFiles.Count));
Mocker.GetMock<IAugmentingService>()
.Verify(c => c.Augment(It.IsAny<LocalEpisode>(), It.IsAny<bool>()), Times.Exactly(_videoFiles.Count));
decisions.Should().HaveCount(3);
decisions.First().Rejections.Should().NotBeEmpty();
}
[Test]
public void should_not_use_folder_for_full_season()
{
var videoFiles = new[]
{
@"C:\Test\Unsorted\Series.Title.S01\S01E01.mkv".AsOsAgnostic(),
@"C:\Test\Unsorted\Series.Title.S01\S01E02.mkv".AsOsAgnostic(),
@"C:\Test\Unsorted\Series.Title.S01\S01E03.mkv".AsOsAgnostic()
};
GivenSpecifications(_pass1);
GivenVideoFiles(videoFiles);
var folderInfo = Parser.Parser.ParseTitle("Series.Title.S01");
Subject.GetImportDecisions(_videoFiles, _series, null, folderInfo, true);
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalEpisode(It.IsAny<string>(), It.IsAny<Series>(), null, true), Times.Exactly(3));
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalEpisode(It.IsAny<string>(), It.IsAny<Series>(), It.Is<ParsedEpisodeInfo>(p => p != null), true), Times.Never());
}
[Test]
public void should_not_use_folder_when_it_contains_more_than_one_valid_video_file()
{
var videoFiles = new[]
{
@"C:\Test\Unsorted\Series.Title.S01E01\S01E01.mkv".AsOsAgnostic(),
@"C:\Test\Unsorted\Series.Title.S01E01\1x01.mkv".AsOsAgnostic()
};
GivenSpecifications(_pass1);
GivenVideoFiles(videoFiles);
var folderInfo = Parser.Parser.ParseTitle("Series.Title.S01E01");
Subject.GetImportDecisions(_videoFiles, _series, null, folderInfo, true);
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalEpisode(It.IsAny<string>(), It.IsAny<Series>(), null, true), Times.Exactly(2));
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalEpisode(It.IsAny<string>(), It.IsAny<Series>(), It.Is<ParsedEpisodeInfo>(p => p != null), true), Times.Never());
}
[Test]
public void should_use_folder_when_only_one_video_file()
{
var videoFiles = new[]
{
@"C:\Test\Unsorted\Series.Title.S01E01\S01E01.mkv".AsOsAgnostic()
};
GivenSpecifications(_pass1);
GivenVideoFiles(videoFiles);
var folderInfo = Parser.Parser.ParseTitle("Series.Title.S01E01");
Subject.GetImportDecisions(_videoFiles, _series, null, folderInfo, true);
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalEpisode(It.IsAny<string>(), It.IsAny<Series>(), It.IsAny<ParsedEpisodeInfo>(), true), Times.Exactly(1));
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalEpisode(It.IsAny<string>(), It.IsAny<Series>(), null, true), Times.Never());
}
[Test]
public void should_use_folder_when_only_one_video_file_and_a_sample()
{
var videoFiles = new[]
{
@"C:\Test\Unsorted\Series.Title.S01E01\S01E01.mkv".AsOsAgnostic(),
@"C:\Test\Unsorted\Series.Title.S01E01\S01E01.sample.mkv".AsOsAgnostic()
};
GivenSpecifications(_pass1);
GivenVideoFiles(videoFiles.ToList());
Mocker.GetMock<IDetectSample>()
.Setup(s => s.IsSample(_series, It.IsAny<string>(), It.IsAny<bool>()))
.Returns((Series s, string path, bool special) =>
{
if (path.Contains("sample"))
{
return DetectSampleResult.Sample;
}
return DetectSampleResult.NotSample;
});
var folderInfo = Parser.Parser.ParseTitle("Series.Title.S01E01");
Subject.GetImportDecisions(_videoFiles, _series, null, folderInfo, true);
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalEpisode(It.IsAny<string>(), It.IsAny<Series>(), It.IsAny<ParsedEpisodeInfo>(), true), Times.Exactly(2));
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalEpisode(It.IsAny<string>(), It.IsAny<Series>(), null, true), Times.Never());
}
[Test]
public void should_not_use_folder_name_if_file_name_is_scene_name()
{
var videoFiles = new[]
{
@"C:\Test\Unsorted\Series.Title.S01E01.720p.HDTV-LOL\Series.Title.S01E01.720p.HDTV-LOL.mkv".AsOsAgnostic()
};
GivenSpecifications(_pass1);
GivenVideoFiles(videoFiles);
var folderInfo = Parser.Parser.ParseTitle("Series.Title.S01E01.720p.HDTV-LOL");
Subject.GetImportDecisions(_videoFiles, _series, null, folderInfo, true);
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalEpisode(It.IsAny<string>(), It.IsAny<Series>(), null, true), Times.Exactly(1));
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalEpisode(It.IsAny<string>(), It.IsAny<Series>(), It.Is<ParsedEpisodeInfo>(p => p != null), true), Times.Never());
}
[Test]
public void should_not_use_folder_quality_when_it_is_unknown()
{
GivenSpecifications(_pass1, _pass2, _pass3);
_series.Profile = new Profile
{
Items = Qualities.QualityFixture.GetDefaultQualities(Quality.DVD, Quality.Unknown)
};
var folderQuality = new QualityModel(Quality.Unknown);
var result = Subject.GetImportDecisions(_videoFiles, _series, null, new ParsedEpisodeInfo { Quality = folderQuality}, true);
result.Single().LocalEpisode.Quality.Should().Be(_quality);
}
[Test]
public void should_return_a_decision_when_exception_is_caught()
{
Mocker.GetMock<IParsingService>()
.Setup(c => c.GetLocalEpisode(It.IsAny<string>(), It.IsAny<Series>(), It.IsAny<ParsedEpisodeInfo>(), It.IsAny<bool>()))
Mocker.GetMock<IAugmentingService>()
.Setup(c => c.Augment(It.IsAny<LocalEpisode>(), It.IsAny<bool>()))
.Throws<TestException>();
_videoFiles = new List<string>

View File

@@ -1,4 +1,4 @@
using FizzWare.NBuilder;
using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.MediaFiles.EpisodeImport.Specifications;
@@ -22,7 +22,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
Path = @"C:\Test\30 Rock\30.rock.s01e01.avi".AsOsAgnostic(),
Size = 100,
Series = Builder<Series>.CreateNew().Build(),
ParsedEpisodeInfo = new ParsedEpisodeInfo
FileEpisodeInfo = new ParsedEpisodeInfo
{
FullSeason = false
}
@@ -32,7 +32,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
[Test]
public void should_return_false_when_file_contains_the_full_season()
{
_localEpisode.ParsedEpisodeInfo.FullSeason = true;
_localEpisode.FileEpisodeInfo.FullSeason = true;
Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeFalse();
}

View File

@@ -1,4 +1,4 @@
using FizzWare.NBuilder;
using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.MediaFiles.EpisodeImport.Specifications;
@@ -18,9 +18,16 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
{
_localEpisode = Builder<LocalEpisode>.CreateNew()
.With(l => l.Path = @"C:\Test\Unsorted\Series.Title.S01E01.720p.HDTV-Sonarr\S01E05.mkv".AsOsAgnostic())
.With(l => l.ParsedEpisodeInfo =
.With(l => l.FileEpisodeInfo =
Builder<ParsedEpisodeInfo>.CreateNew()
.With(p => p.EpisodeNumbers = new[] {5})
.With(p => p.EpisodeNumbers = new[] { 5 })
.With(p => p.SeasonNumber == 1)
.With(p => p.FullSeason = false)
.Build())
.With(l => l.FolderEpisodeInfo =
Builder<ParsedEpisodeInfo>.CreateNew()
.With(p => p.EpisodeNumbers = new[] { 1 })
.With(p => p.SeasonNumber == 1)
.With(p => p.FullSeason = false)
.Build())
.Build();
@@ -38,6 +45,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
public void should_be_accepted_if_folder_name_is_not_parseable()
{
_localEpisode.Path = @"C:\Test\Unsorted\Series.Title\S01E01.mkv".AsOsAgnostic();
_localEpisode.FolderEpisodeInfo = null;
Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeTrue();
}
@@ -46,6 +54,8 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
public void should_should_be_accepted_for_full_season()
{
_localEpisode.Path = @"C:\Test\Unsorted\Series.Title.S01\S01E01.mkv".AsOsAgnostic();
_localEpisode.FolderEpisodeInfo.EpisodeNumbers = new int[0];
_localEpisode.FolderEpisodeInfo.FullSeason = true;
Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeTrue();
}
@@ -53,32 +63,64 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
[Test]
public void should_be_accepted_if_file_and_folder_have_the_same_episode()
{
_localEpisode.ParsedEpisodeInfo.EpisodeNumbers = new[] { 1 };
_localEpisode.FileEpisodeInfo.EpisodeNumbers = new[] { 1 };
_localEpisode.FolderEpisodeInfo.EpisodeNumbers = new[] { 1 };
_localEpisode.Path = @"C:\Test\Unsorted\Series.Title.S01E01.720p.HDTV-Sonarr\S01E01.mkv".AsOsAgnostic();
Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeTrue();
}
[Test]
public void should_be_accepted_if_file_is_one_episode_in_folder()
{
_localEpisode.ParsedEpisodeInfo.EpisodeNumbers = new[] { 1 };
_localEpisode.FileEpisodeInfo.EpisodeNumbers = new[] { 1 };
_localEpisode.FolderEpisodeInfo.EpisodeNumbers = new[] { 1 };
_localEpisode.Path = @"C:\Test\Unsorted\Series.Title.S01E01E02.720p.HDTV-Sonarr\S01E01.mkv".AsOsAgnostic();
Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeTrue();
Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeTrue();
}
[Test]
public void should_be_disregard_subfolder()
{
_localEpisode.FileEpisodeInfo.EpisodeNumbers = new[] { 5, 6 };
_localEpisode.FolderEpisodeInfo.EpisodeNumbers = new[] { 1, 2 };
_localEpisode.Path = @"C:\Test\Unsorted\Series.Title.S01E01E02.720p.HDTV-Sonarr\S01E05E06.mkv".AsOsAgnostic();
Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeFalse();
}
[Test]
public void should_be_rejected_if_file_and_folder_do_not_have_same_episode()
{
_localEpisode.Path = @"C:\Test\Unsorted\Series.Title.S01E01.720p.HDTV-Sonarr\S01E05.mkv".AsOsAgnostic();
Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeFalse();
Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeFalse();
}
[Test]
public void should_be_rejected_if_file_and_folder_do_not_have_same_episodes()
public void should_be_rejected_if_file_and_folder_do_not_have_the_same_episodes()
{
_localEpisode.ParsedEpisodeInfo.EpisodeNumbers = new[] { 5, 6 };
_localEpisode.FileEpisodeInfo.EpisodeNumbers = new[] { 5, 6 };
_localEpisode.FolderEpisodeInfo.EpisodeNumbers = new[] { 1, 2 };
_localEpisode.Path = @"C:\Test\Unsorted\Series.Title.S01E01E02.720p.HDTV-Sonarr\S01E05E06.mkv".AsOsAgnostic();
Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeFalse();
}
[Test]
public void should_be_rejected_if_file_and_folder_do_not_have_episodes_from_the_same_season()
{
_localEpisode.FileEpisodeInfo.SeasonNumber = 2;
_localEpisode.FileEpisodeInfo.EpisodeNumbers = new[] { 1 };
_localEpisode.FolderEpisodeInfo.FullSeason = true;
_localEpisode.FolderEpisodeInfo.SeasonNumber = 1;
_localEpisode.FolderEpisodeInfo.EpisodeNumbers = new[] { 1, 2 };
_localEpisode.Path = @"C:\Test\Unsorted\Series.Title.S01.720p.HDTV-Sonarr\S02E01.mkv".AsOsAgnostic();
Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeFalse();
}
}
}
}

View File

@@ -58,10 +58,7 @@ namespace NzbDrone.Core.Test.MediaFiles
Episodes = new List<Episode> { episode },
Path = Path.Combine(series.Path, "30 Rock - S01E01 - Pilot.avi"),
Quality = new QualityModel(Quality.Bluray720p),
ParsedEpisodeInfo = new ParsedEpisodeInfo
{
ReleaseGroup = "DRONE"
}
ReleaseGroup = "DRONE"
}));
}

View File

@@ -15,7 +15,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo.MediaInfoFormatterTests
[TestCase("V_MPEGH/ISO/HEVC", "source.title.x265.720p-Sonarr", "x265")]
[TestCase("V_MPEGH/ISO/HEVC", "source.title.h265.720p-Sonarr", "h265")]
[TestCase("MPEG-2 Video", null, "MPEG2")]
public void should_format_video_codec_with_source_title(string videoCodec, string sceneName, string expectedFormat)
public void should_format_video_codec_with_source_title_legacy(string videoCodec, string sceneName, string expectedFormat)
{
var mediaInfoModel = new MediaInfoModel
{
@@ -39,6 +39,11 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo.MediaInfoFormatterTests
[TestCase("VP7, VP70, General, ", "Sweet Seymour.avi", "VP7")]
[TestCase("VP8, V_VP8, , ", "Dick.mkv", "VP8")]
[TestCase("VP9, V_VP9, , ", "Roadkill Ep3x11 - YouTube.webm", "VP9")]
[TestCase("x264, x264, , ", "Ghost Advent - S04E05 - Stanley Hotel SDTV.avi", "x264")]
[TestCase("V_MPEGH/ISO/HEVC, V_MPEGH/ISO/HEVC, , ", "The BBT S11E12 The Matrimonial Metric 1080p 10bit AMZN WEB-DL", "h265")]
[TestCase("MPEG-4 Visual, 20, Simple@L1, Lavc52.29.0", "Will.And.Grace.S08E14.WS.DVDrip.XviD.I.Love.L.Gay-Obfuscated", "XviD")]
[TestCase("MPEG-4 Visual, 20, Advanced Simple@L5, XviD0046", "", "XviD")]
[TestCase("mp4v, mp4v, , ", "American.Chopper.S06E07.Mountain.Creek.Bike.DSR.XviD-KRS", "XviD")]
public void should_format_video_format(string videoFormatPack, string sceneName, string expectedFormat)
{
var split = videoFormatPack.Split(new string[] { ", " }, System.StringSplitOptions.None);
@@ -53,6 +58,42 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo.MediaInfoFormatterTests
MediaInfoFormatter.FormatVideoCodec(mediaInfoModel, sceneName).Should().Be(expectedFormat);
}
[TestCase("AVC, AVC, , x264", "Some.Video.S01E01.h264", "x264")] // Force mediainfo tag
[TestCase("HEVC, HEVC, , x265", "Some.Video.S01E01.h265", "x265")] // Force mediainfo tag
[TestCase("AVC, AVC, , ", "Some.Video.S01E01.x264", "x264")] // Not seen in practice, but honor tag if otherwise unknown
[TestCase("HEVC, HEVC, , ", "Some.Video.S01E01.x265", "x265")] // Not seen in practice, but honor tag if otherwise unknown
[TestCase("AVC, AVC, , ", "Some.Video.S01E01", "h264")] // Default value
[TestCase("HEVC, HEVC, , ", "Some.Video.S01E01", "h265")] // Default value
public void should_format_video_format_fallbacks(string videoFormatPack, string sceneName, string expectedFormat)
{
var split = videoFormatPack.Split(new string[] { ", " }, System.StringSplitOptions.None);
var mediaInfoModel = new MediaInfoModel
{
VideoFormat = split[0],
VideoCodecID = split[1],
VideoProfile = split[2],
VideoCodecLibrary = split[3]
};
MediaInfoFormatter.FormatVideoCodec(mediaInfoModel, sceneName).Should().Be(expectedFormat);
}
[TestCase("MPEG-4 Visual, 20, , Intel(R) MPEG-4 encoder based on Intel(R) IPP 6.1 build 137.20[6.1.137.763]", "", "")]
public void should_warn_on_unknown_video_format(string videoFormatPack, string sceneName, string expectedFormat)
{
var split = videoFormatPack.Split(new string[] { ", " }, System.StringSplitOptions.None);
var mediaInfoModel = new MediaInfoModel
{
VideoFormat = split[0],
VideoCodecID = split[1],
VideoProfile = split[2],
VideoCodecLibrary = split[3]
};
MediaInfoFormatter.FormatVideoCodec(mediaInfoModel, sceneName).Should().Be(expectedFormat);
ExceptionVerification.ExpectedWarns(1);
}
[Test]
public void should_return_VideoFormat_by_default()
{

View File

@@ -90,14 +90,19 @@
<Private>True</Private>
</Reference>
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<HintPath>..\packages\NLog.4.5.0-rc06\lib\net40-client\NLog.dll</HintPath>
<HintPath>..\packages\NLog.4.5.3\lib\net40-client\NLog.dll</HintPath>
</Reference>
<Reference Include="nunit.framework, Version=3.6.0.0, Culture=neutral, PublicKeyToken=2638cd05610744eb, processorArchitecture=MSIL">
<HintPath>..\packages\NUnit.3.6.0\lib\net40\nunit.framework.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Configuration" />
<Reference Include="System.Data" />
<Reference Include="System.Drawing" />
<Reference Include="System.Numerics" />
<Reference Include="System.Runtime.Serialization" />
<Reference Include="System.ServiceModel" />
<Reference Include="System.Transactions" />
<Reference Include="System.Xml" />
<Reference Include="System.Xml.Linq" />
<Reference Include="Microsoft.CSharp" />
@@ -148,6 +153,7 @@
<Compile Include="Datastore\Migration\079_dedupe_tagsFixture.cs" />
<Compile Include="Datastore\Migration\075_force_lib_updateFixture.cs" />
<Compile Include="Datastore\Migration\090_update_kickass_urlFixture.cs" />
<Compile Include="Datastore\Migration\121_update_animetosho_urlFixture.cs" />
<Compile Include="Datastore\ObjectDatabaseFixture.cs" />
<Compile Include="Datastore\PagingSpecExtensionsTests\PagingOffsetFixture.cs" />
<Compile Include="Datastore\PagingSpecExtensionsTests\ToSortDirectionFixture.cs" />
@@ -220,6 +226,7 @@
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<Compile Include="Download\TrackedDownloads\TrackedDownloadServiceFixture.cs" />
<Compile Include="Extras\Metadata\Consumers\Xbmc\FindMetadataFileFixture.cs" />
<Compile Include="FluentTest.cs" />
<Compile Include="Framework\CoreTest.cs" />
<Compile Include="Framework\DbTest.cs" />
@@ -235,6 +242,7 @@
<Compile Include="HealthCheck\Checks\ImportMechanismCheckFixture.cs" />
<Compile Include="HealthCheck\Checks\IndexerSearchCheckFixture.cs" />
<Compile Include="HealthCheck\Checks\IndexerRssCheckFixture.cs" />
<Compile Include="HealthCheck\Checks\MonoDebugFixture.cs" />
<Compile Include="HealthCheck\Checks\MonoVersionCheckFixture.cs" />
<Compile Include="HealthCheck\Checks\IndexerStatusCheckFixture.cs" />
<Compile Include="HealthCheck\Checks\RootFolderCheckFixture.cs" />
@@ -271,6 +279,7 @@
<Compile Include="IndexerTests\IntegrationTests\IndexerIntegrationTests.cs" />
<Compile Include="IndexerTests\NewznabTests\NewznabCapabilitiesProviderFixture.cs" />
<Compile Include="IndexerTests\RarbgTests\RarbgFixture.cs" />
<Compile Include="IndexerTests\SeedConfigProviderFixture.cs" />
<Compile Include="IndexerTests\TorrentRssIndexerTests\TorrentRssParserFactoryFixture.cs" />
<Compile Include="IndexerTests\TorrentRssIndexerTests\TorrentRssSettingsDetectorFixture.cs" />
<Compile Include="IndexerTests\TorznabTests\TorznabFixture.cs" />
@@ -298,6 +307,9 @@
<Compile Include="MediaFiles\DownloadedEpisodesCommandServiceFixture.cs" />
<Compile Include="MediaFiles\DownloadedEpisodesImportServiceFixture.cs" />
<Compile Include="MediaFiles\EpisodeFileMovingServiceTests\MoveEpisodeFileFixture.cs" />
<Compile Include="MediaFiles\EpisodeImport\Aggregation\Aggregators\AggregateEpisodesFixture.cs" />
<Compile Include="MediaFiles\EpisodeImport\Aggregation\Aggregators\AggregateQualityFixture.cs" />
<Compile Include="MediaFiles\EpisodeImport\Aggregation\Aggregators\Augmenters\Quality\AugmentQualityFromMediaInfoFixture.cs" />
<Compile Include="MediaFiles\EpisodeImport\ImportDecisionMakerFixture.cs" />
<Compile Include="MediaFiles\EpisodeImport\DetectSampleFixture.cs" />
<Compile Include="MediaFiles\EpisodeImport\Specifications\EpisodeTitleSpecificationFixture.cs" />
@@ -327,10 +339,11 @@
<Compile Include="OrganizerTests\FileNameBuilderTests\MultiEpisodeFixture.cs" />
<Compile Include="OrganizerTests\FileNameBuilderTests\TitleTheFixture.cs" />
<Compile Include="ParserTests\MiniSeriesEpisodeParserFixture.cs" />
<Compile Include="ParserTests\ValidateParsedEpisodeInfoFixture.cs" />
<Compile Include="Qualities\RevisionComparableFixture.cs" />
<Compile Include="QueueTests\QueueServiceFixture.cs" />
<Compile Include="RemotePathMappingsTests\RemotePathMappingServiceFixture.cs" />
<Compile Include="OrganizerTests\CleanFixture.cs" />
<Compile Include="OrganizerTests\CleanFilenameFixture.cs" />
<Compile Include="MediaFiles\MediaFileServiceTests\FilterFixture.cs" />
<Compile Include="MediaFiles\MediaFileTableCleanupServiceFixture.cs" />
<Compile Include="MediaFiles\MediaInfo\UpdateMediaInfoServiceFixture.cs" />
@@ -340,8 +353,8 @@
<Compile Include="Messaging\Commands\CommandEqualityComparerFixture.cs" />
<Compile Include="Messaging\Commands\CommandExecutorFixture.cs" />
<Compile Include="Messaging\Events\EventAggregatorFixture.cs" />
<Compile Include="Metadata\Consumers\Roksbox\FindMetadataFileFixture.cs" />
<Compile Include="Metadata\Consumers\Wdtv\FindMetadataFileFixture.cs" />
<Compile Include="Extras\Metadata\Consumers\Roksbox\FindMetadataFileFixture.cs" />
<Compile Include="Extras\Metadata\Consumers\Wdtv\FindMetadataFileFixture.cs" />
<Compile Include="NotificationTests\PlexClientServiceTest.cs" />
<Compile Include="NotificationTests\ProwlProviderTest.cs" />
<Compile Include="NotificationTests\Xbmc\Http\ActivePlayersFixture.cs" />

View File

@@ -0,0 +1,30 @@
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.OrganizerTests
{
[TestFixture]
public class CleanFilenameFixture : CoreTest
{
[TestCase("Law & Order: Criminal Intent - S10E07 - Icarus [HDTV-720p]", "Law & Order- Criminal Intent - S10E07 - Icarus [HDTV-720p]")]
public void should_replaace_invalid_characters(string name, string expectedName)
{
FileNameBuilder.CleanFileName(name).Should().Be(expectedName);
}
[TestCase(".hack s01e01", "hack s01e01")]
public void should_remove_periods_from_start(string name, string expectedName)
{
FileNameBuilder.CleanFileName(name).Should().Be(expectedName);
}
[TestCase(" Series Title - S01E01 - Episode Title", "Series Title - S01E01 - Episode Title")]
[TestCase("Series Title - S01E01 - Episode Title ", "Series Title - S01E01 - Episode Title")]
public void should_remove_spaces_from_start_and_end(string name, string expectedName)
{
FileNameBuilder.CleanFileName(name).Should().Be(expectedName);
}
}
}

View File

@@ -1,19 +0,0 @@
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.OrganizerTests
{
[TestFixture]
public class CleanFixture : CoreTest
{
[TestCase("Law & Order: Criminal Intent - S10E07 - Icarus [HDTV-720p]",
"Law & Order- Criminal Intent - S10E07 - Icarus [HDTV-720p]")]
public void CleanFileName(string name, string expectedName)
{
FileNameBuilder.CleanFileName(name).Should().Be(expectedName);
}
}
}

View File

@@ -86,6 +86,9 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Dragon Ball Super Episode 56 [VOSTFR V2][720p][AAC]-Mystic Z-Team", "Dragon Ball Super", 56, 0, 0)]
[TestCase("[Mystic Z-Team] Dragon Ball Super Episode 69 [VOSTFR_Finale][1080p][AAC].mp4", "Dragon Ball Super", 69, 0, 0)]
[TestCase("[Shark-Raws] Crayon Shin-chan #957 (NBN 1280x720 x264 AAC).mp4", "Crayon Shin-chan", 957, 0, 0)]
[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("", "", 0, 0, 0)]
public void should_parse_absolute_numbers(string postTitle, string title, int absoluteEpisodeNumber, int seasonNumber, int episodeNumber)
{

View File

@@ -59,6 +59,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Mad.Men.S05E01-02.720p.5.1Ch.BluRay", "Mad Men", 5, new[] { 1, 2 })]
[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("", "", , new [] { })]
public void should_parse_multiple_episodes(string postTitle, string title, int season, int[] episodes)
{

View File

@@ -37,6 +37,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Match of the Day", "matchday")]
[TestCase("Match of the Day 2", "matchday2")]
[TestCase("[ www.Torrenting.com ] - Revenge.S03E14.720p.HDTV.X264-DIMENSION", "Revenge")]
[TestCase("www.Torrenting.com - Revenge.S03E14.720p.HDTV.X264-DIMENSION", "Revenge")]
[TestCase("Seed S02E09 HDTV x264-2HD [eztv]-[rarbg.com]", "Seed")]
[TestCase("Reno.911.S01.DVDRip.DD2.0.x264-DEEP", "Reno 911")]
public void should_parse_series_name(string postTitle, string title)
@@ -70,7 +71,7 @@ namespace NzbDrone.Core.Test.ParserTests
public void should_parse_quality_from_extension(string title)
{
Parser.Parser.ParseTitle(title).Quality.Quality.Should().NotBe(Quality.Unknown);
Parser.Parser.ParseTitle(title).Quality.QualitySource.Should().Be(QualitySource.Extension);
Parser.Parser.ParseTitle(title).Quality.QualityDetectionSource.Should().Be(QualityDetectionSource.Extension);
}

View File

@@ -177,6 +177,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("The.Simpsons.S25E21.Pay.Pal.1080p.WEB-DL.DD5.1.H.264-NTb", false)]
[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)]
public void should_parse_webdl1080p_quality(string title, bool proper)
{
ParseAndVerifyQuality(title, Quality.WEBDL1080p, proper);
@@ -283,7 +284,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("White.Van.Man.2011.S02E01.WS.PDTV.x264-REPACK-TLA")]
public void should_parse_quality_from_name(string title)
{
QualityParser.ParseQuality(title).QualitySource.Should().Be(QualitySource.Name);
QualityParser.ParseQuality(title).QualityDetectionSource.Should().Be(QualityDetectionSource.Name);
}
[TestCase("Revolution.S01E02.Chained.Heat.mkv")]
@@ -292,7 +293,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("[CR] Sailor Moon - 004 [48CE2D0F].avi")]
public void should_parse_quality_from_extension(string title)
{
QualityParser.ParseQuality(title).QualitySource.Should().Be(QualitySource.Extension);
QualityParser.ParseQuality(title).QualityDetectionSource.Should().Be(QualityDetectionSource.Extension);
}
private void ParseAndVerifyQuality(string title, Quality quality, bool proper)

View File

@@ -130,6 +130,10 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Shortland.Street.S26E022.HDTV.x264-FiHTV", "Shortland Street", 26, 22)]
[TestCase("Super.Potatoes.S01.Ep06.1080p.BluRay.DTS.x264-MiR", "Super Potatoes", 1, 6)]
[TestCase("Room 104 - S01E07 The Missionaries [SDTV]", "Room 104", 1, 7)]
[TestCase("11-02 The Retraction Reaction (HD).m4v", "", 11, 2)]
[TestCase("Plus belle la vie - S14E3533 FRENCH WEBRIP H.264 AAC (09.05.2018)", "Plus belle la vie", 14, 3533)]
[TestCase("The 100 - S01E02 - Earth Skills HDTV-1080p AVC DTS [EN+FR+ES+PT+DA+FI+NB+SV]", "The 100", 1, 2)]
[TestCase("Series Title - S01E01 - Day 100 [SDTV]", "Series Title", 1, 1)]
//[TestCase("", "", 0, 0)]
public void should_parse_single_episode(string postTitle, string title, int seasonNumber, int episodeNumber)
{

View File

@@ -0,0 +1,73 @@
using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.ParserTests
{
[TestFixture]
public class ValidateParsedEpisodeInfoFixture : CoreTest
{
private ParsedEpisodeInfo _parsedEpisodeInfo;
private Series _series;
[SetUp]
public void Setup()
{
_parsedEpisodeInfo = Builder<ParsedEpisodeInfo>.CreateNew()
.With(p => p.AirDate = null)
.Build();
_series = Builder<Series>.CreateNew()
.With(s => s.SeriesType = SeriesTypes.Standard)
.Build();
}
private void GivenDailyParsedEpisodeInfo()
{
_parsedEpisodeInfo.AirDate = "2018-05-21";
}
private void GivenDailySeries()
{
_series.SeriesType = SeriesTypes.Daily;
}
[Test]
public void should_return_true_if_episode_info_is_not_daily()
{
ValidateParsedEpisodeInfo.ValidateForSeriesType(_parsedEpisodeInfo, _series).Should().BeTrue();
}
[Test]
public void should_return_true_if_episode_info_is_daily_for_daily_series()
{
GivenDailyParsedEpisodeInfo();
GivenDailySeries();
ValidateParsedEpisodeInfo.ValidateForSeriesType(_parsedEpisodeInfo, _series).Should().BeTrue();
}
[Test]
public void should_return_false_if_episode_info_is_daily_for_standard_series()
{
GivenDailyParsedEpisodeInfo();
ValidateParsedEpisodeInfo.ValidateForSeriesType(_parsedEpisodeInfo, _series).Should().BeFalse();
ExceptionVerification.ExpectedWarns(1);
}
[Test]
public void should_not_log_warning_if_warnIfInvalid_is_false()
{
GivenDailyParsedEpisodeInfo();
ValidateParsedEpisodeInfo.ValidateForSeriesType(_parsedEpisodeInfo, _series, false);
ExceptionVerification.ExpectedWarns(0);
}
}
}

View File

@@ -1,11 +1,14 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Exceptions;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MetadataSource;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
@@ -180,5 +183,35 @@ namespace NzbDrone.Core.Test.TvTests
.Verify(v => v.UpdateSeries(It.Is<Series>(s => s.Seasons.Count == 2), It.IsAny<bool>()));
}
[Test]
public void should_rescan_series_if_updating_fails()
{
Mocker.GetMock<IProvideSeriesInfo>()
.Setup(s => s.GetSeriesInfo(_series.Id))
.Throws(new IOException());
Assert.Throws<IOException>(() => Subject.Execute(new RefreshSeriesCommand(_series.Id)));
Mocker.GetMock<IDiskScanService>()
.Verify(v => v.Scan(_series), Times.Once());
ExceptionVerification.ExpectedErrors(1);
}
[Test]
public void should_not_rescan_series_if_updating_fails_with_series_not_found()
{
Mocker.GetMock<IProvideSeriesInfo>()
.Setup(s => s.GetSeriesInfo(_series.Id))
.Throws(new SeriesNotFoundException(_series.Id));
Subject.Execute(new RefreshSeriesCommand(_series.Id));
Mocker.GetMock<IDiskScanService>()
.Verify(v => v.Scan(_series), Times.Never());
ExceptionVerification.ExpectedErrors(1);
}
}
}

View File

@@ -9,7 +9,7 @@
<package id="Moq" version="4.0.10827" targetFramework="net40" />
<package id="NBuilder" version="4.0.0" targetFramework="net40" />
<package id="Newtonsoft.Json" version="9.0.1" targetFramework="net40" />
<package id="NLog" version="4.5.0-rc06" targetFramework="net40" />
<package id="NLog" version="4.5.3" targetFramework="net40" />
<package id="NUnit" version="3.6.0" targetFramework="net40" />
<package id="Prowlin" version="0.9.4456.26422" targetFramework="net40" />
<package id="Unity" version="2.1.505.2" targetFramework="net40" />

View File

@@ -12,6 +12,7 @@ namespace NzbDrone.Core.Annotations
public int Order { get; private set; }
public string Label { get; set; }
public string Unit { get; set; }
public string HelpText { get; set; }
public string HelpLink { get; set; }
public FieldType Type { get; set; }
@@ -33,4 +34,4 @@ namespace NzbDrone.Core.Annotations
Url,
Captcha
}
}
}

View File

@@ -27,10 +27,20 @@ namespace NzbDrone.Core.Datastore
static DbFactory()
{
InitializeEnvironment();
MapRepository.Instance.ReflectionStrategy = new SimpleReflectionStrategy();
TableMapping.Map();
}
private static void InitializeEnvironment()
{
// Speed up sqlite3 initialization since we don't use the config file and can't rely on preloading.
Environment.SetEnvironmentVariable("No_Expand", "true");
Environment.SetEnvironmentVariable("No_SQLiteXmlConfigFile", "true");
Environment.SetEnvironmentVariable("No_PreLoadSQLite", "true");
}
public static void RegisterDatabase(IContainer container)
{
var mainDb = new MainDatabase(container.Resolve<IDbFactory>().Create());

View File

@@ -0,0 +1,23 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(120)]
public class update_series_episodes_history_indexes : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Create.Index().OnTable("Episodes").OnColumn("SeriesId").Ascending()
.OnColumn("AirDate").Ascending();
Delete.Index().OnTable("History").OnColumn("EpisodeId");
Create.Index().OnTable("History").OnColumn("EpisodeId").Ascending()
.OnColumn("Date").Descending();
Delete.Index().OnTable("History").OnColumn("DownloadId");
Create.Index().OnTable("History").OnColumn("DownloadId").Ascending()
.OnColumn("Date").Descending();
}
}
}

View File

@@ -0,0 +1,22 @@
using FluentMigrator;
using Newtonsoft.Json.Linq;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(121)]
public class update_animetosho_url : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Execute.Sql("UPDATE Indexers SET Settings = Replace(Replace(Settings, '//animetosho.org', '//feed.animetosho.org'), '/feed/nabapi', '/nabapi') WHERE (Implementation = 'Newznab' OR Implementation = 'Torznab') AND Settings LIKE '%animetosho%';");
}
}
public class NewznabSettings121
{
public string BaseUrl { get; set; }
public string ApiPath { get; set; }
}
}

View File

@@ -71,7 +71,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
//Multiply maxSize by Series.Runtime
maxSize = maxSize * subject.Series.Runtime * subject.Episodes.Count;
if (subject.Episodes.Count == 1)
if (subject.Episodes.Count == 1 && subject.Series.SeriesType == SeriesTypes.Standard)
{
Episode episode = subject.Episodes.First();
List<Episode> seasonEpisodes;
@@ -79,7 +79,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
var seasonSearchCriteria = searchCriteria as SeasonSearchCriteria;
if (seasonSearchCriteria != null && !seasonSearchCriteria.Series.UseSceneNumbering && seasonSearchCriteria.Episodes.Any(v => v.Id == episode.Id))
{
seasonEpisodes = (searchCriteria as SeasonSearchCriteria).Episodes;
seasonEpisodes = seasonSearchCriteria.Episodes;
}
else
{

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
@@ -82,9 +82,6 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
public override string Name => "Torrent Blackhole";
public override ProviderMessage Message => new ProviderMessage("Magnet links are not supported.", ProviderMessageType.Warning);
public override IEnumerable<DownloadClientItem> GetItems()
{
foreach (var item in _scanWatchFolder.GetItems(Settings.WatchFolder, ScanGracePeriod))

View File

@@ -45,6 +45,8 @@ namespace NzbDrone.Core.Download.Clients.Deluge
_proxy.SetLabel(actualHash, Settings.TvCategory, Settings);
}
_proxy.SetTorrentSeedingConfiguration(actualHash, remoteEpisode.SeedConfiguration, Settings);
var isRecentEpisode = remoteEpisode.IsRecentEpisode();
if (isRecentEpisode && Settings.RecentTvPriority == (int)DelugePriority.First ||
@@ -65,6 +67,8 @@ namespace NzbDrone.Core.Download.Clients.Deluge
throw new DownloadClientException("Deluge failed to add torrent " + filename);
}
_proxy.SetTorrentSeedingConfiguration(actualHash, remoteEpisode.SeedConfiguration, Settings);
if (!Settings.TvCategory.IsNullOrWhiteSpace())
{
_proxy.SetLabel(actualHash, Settings.TvCategory, Settings);
@@ -110,6 +114,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
var outputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.DownloadPath));
item.OutputPath = outputPath + torrent.Name;
item.RemainingSize = torrent.Size - torrent.BytesDownloaded;
item.SeedRatio = torrent.Ratio;
try
{
@@ -145,8 +150,13 @@ namespace NzbDrone.Core.Download.Clients.Deluge
item.Status = DownloadItemStatus.Downloading;
}
// Here we detect if Deluge is managing the torrent and whether the seed criteria has been met. This allows drone to delete the torrent as appropriate.
item.CanMoveFiles = item.CanBeRemoved = (torrent.IsAutoManaged && torrent.StopAtRatio && torrent.Ratio >= torrent.StopRatio && torrent.State == DelugeTorrentStatus.Paused);
// Here we detect if Deluge is managing the torrent and whether the seed criteria has been met.
// This allows drone to delete the torrent as appropriate.
item.CanMoveFiles = item.CanBeRemoved =
torrent.IsAutoManaged &&
torrent.StopAtRatio &&
torrent.Ratio >= torrent.StopRatio &&
torrent.State == DelugeTorrentStatus.Paused;
items.Add(item);
}

View File

@@ -151,13 +151,17 @@ namespace NzbDrone.Core.Download.Clients.Deluge
public void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, DelugeSettings settings)
{
if (seedConfiguration == null) return;
var ratioArguments = new Dictionary<string, object>();
if (seedConfiguration.Ratio != null)
{
var ratioArguments = new Dictionary<string, object>();
ratioArguments.Add("stop_ratio", seedConfiguration.Ratio.Value);
ProcessRequest<object>(settings, "core.set_torrent_options", new string[] { hash }, ratioArguments);
ratioArguments.Add("stop_at_ratio", 1);
}
ProcessRequest<object>(settings, "core.set_torrent_options", new[] { hash }, ratioArguments);
}
public void AddLabel(string label, DelugeSettings settings)
@@ -176,7 +180,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
var requestBuilder = new JsonRpcRequestBuilder(url);
requestBuilder.LogResponseContent = true;
requestBuilder.Resource("json");
requestBuilder.PostProcess += r => r.RequestTimeout = TimeSpan.FromSeconds(15);

View File

@@ -13,7 +13,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
[JsonProperty(PropertyName = "is_finished")]
public bool IsFinished { get; set; }
// Other paths: What is the difference between 'move_completed_path' and 'move_on_completed_path'?
/*
[JsonProperty(PropertyName = "move_completed_path")]
@@ -22,7 +22,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
public String DownloadPathMoveOnCompleted { get; set; }
*/
[JsonProperty(PropertyName = "save_path")]
[JsonProperty(PropertyName = "save_path")]
public string DownloadPath { get; set; }
[JsonProperty(PropertyName = "total_size")]

View File

@@ -88,6 +88,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
TotalSize = torrent.Size,
RemainingSize = GetRemainingSize(torrent),
RemainingTime = GetRemainingTime(torrent),
SeedRatio = GetSeedRatio(torrent),
Status = GetStatus(torrent),
Message = GetMessage(torrent),
CanMoveFiles = IsCompleted(torrent),
@@ -121,7 +122,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
{
_logger.Debug(e, "Failed to get config from Download Station");
throw e;
throw;
}
}
@@ -278,6 +279,19 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
return TimeSpan.FromSeconds(remainingSize / downloadSpeed);
}
protected double? GetSeedRatio(DownloadStationTask torrent)
{
var downloaded = torrent.Additional.Transfer["size_downloaded"].ParseInt64();
var uploaded = torrent.Additional.Transfer["size_uploaded"].ParseInt64();
if (downloaded.HasValue && uploaded.HasValue)
{
return downloaded <= 0 ? 0 : (double)uploaded.Value / downloaded.Value;
}
return null;
}
protected ValidationFailure TestOutputPath()
{
try

View File

@@ -147,7 +147,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
{
_logger.Debug(e, "Failed to get config from Download Station");
throw e;
throw;
}
}

View File

@@ -62,7 +62,9 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
RemainingSize = torrent.TotalSize - torrent.DownloadedBytes,
RemainingTime = eta,
Title = torrent.Name,
TotalSize = torrent.TotalSize
TotalSize = torrent.TotalSize,
SeedRatio = torrent.DownloadedBytes <= 0 ? 0 :
(double) torrent.UploadedBytes / torrent.DownloadedBytes
};
if (!string.IsNullOrEmpty(torrent.Error))

View File

@@ -138,6 +138,7 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
TotalSize = Convert.ToInt64(item[3]),
Progress = Convert.ToDouble(item[4]),
DownloadedBytes = Convert.ToInt64(item[5]),
UploadedBytes = Convert.ToInt64(item[6]),
DownloadRate = Convert.ToInt64(item[9]),
Label = Convert.ToString(item[11]),
Error = Convert.ToString(item[21]),

View File

@@ -13,6 +13,7 @@
public bool IsSeeding { get; set; }
public long TotalSize { get; set; }
public long DownloadedBytes { get; set; }
public long UploadedBytes { get; set; }
public long DownloadRate { get; set; }
public string Error { get; set; }
}

View File

@@ -84,7 +84,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
_logger.Warn(ex, "Failed to set the torrent priority for {0}.", filename);
}
SetInitialState(hash);
SetInitialState(hash.ToLower());
return hash;
}
@@ -107,7 +107,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
item.TotalSize = torrent.Size;
item.DownloadClient = Definition.Name;
item.RemainingSize = (long)(torrent.Size * (1.0 - torrent.Progress));
item.RemainingTime = TimeSpan.FromSeconds(torrent.Eta);
item.RemainingTime = GetRemainingTime(torrent);
item.SeedRatio = torrent.Ratio;
item.OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.SavePath));
@@ -333,5 +334,15 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
_logger.Warn(ex, "Failed to set inital state for {0}.", hash);
}
}
protected TimeSpan? GetRemainingTime(QBittorrentTorrent torrent)
{
if (torrent.Eta < 0 || torrent.Eta > 365 * 24 * 3600)
{
return null;
}
return TimeSpan.FromSeconds((int)torrent.Eta);
}
}
}

View File

@@ -80,6 +80,11 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
request.AddFormParameter("category", settings.TvCategory);
}
if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
{
request.AddFormParameter("paused", true);
}
var result = ProcessRequest(request, settings);
// Note: Older qbit versions returned nothing, so we can't do != "Ok." here.
@@ -95,13 +100,18 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
.Post()
.AddFormUpload("torrents", fileName, fileContent);
var result = ProcessRequest(request, settings);
if (settings.TvCategory.IsNotNullOrWhiteSpace())
{
request.AddFormParameter("category", settings.TvCategory);
}
if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
{
request.AddFormParameter("paused", true);
}
var result = ProcessRequest(request, settings);
// Note: Current qbit versions return nothing, so we can't do != "Ok." here.
if (result == "Fails.")
{
@@ -156,7 +166,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
catch (DownloadClientException ex)
{
// qBittorrent rejects all Prio commands with 403: Forbidden if Options -> BitTorrent -> Torrent Queueing is not enabled
#warning FIXME: so wouldn't the reauthenticate logic trigger on Forbidden?
#warning FIXME: so wouldn't the reauthenticate logic trigger on Forbidden?
if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.Forbidden)
{
return;

View File

@@ -1,4 +1,5 @@
using Newtonsoft.Json;
using System.Numerics;
using Newtonsoft.Json;
namespace NzbDrone.Core.Download.Clients.QBittorrent
{
@@ -13,7 +14,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
public double Progress { get; set; } // Torrent progress (%/100)
public ulong Eta { get; set; } // Torrent ETA (seconds)
public BigInteger Eta { get; set; } // Torrent ETA (seconds) (QBit contains a bug exceeding ulong limits)
public string State { get; set; } // Torrent state. See possible values here below

View File

@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Disk;
@@ -66,6 +65,9 @@ namespace NzbDrone.Core.Download.Clients.Transmission
item.OutputPath = GetOutputPath(outputPath, torrent);
item.TotalSize = torrent.TotalSize;
item.RemainingSize = torrent.LeftUntilDone;
item.SeedRatio = torrent.DownloadedEver <= 0 ? 0 :
(double) torrent.UploadedEver / torrent.DownloadedEver;
if (torrent.Eta >= 0)
{
item.RemainingTime = TimeSpan.FromSeconds(torrent.Eta);
@@ -96,7 +98,9 @@ namespace NzbDrone.Core.Download.Clients.Transmission
item.Status = DownloadItemStatus.Downloading;
}
item.CanMoveFiles = item.CanBeRemoved = torrent.Status == TransmissionTorrentStatus.Stopped;
item.CanMoveFiles = item.CanBeRemoved =
torrent.Status == TransmissionTorrentStatus.Stopped &&
item.SeedRatio >= torrent.SeedRatioLimit;
items.Add(item);
}
@@ -129,6 +133,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
protected override string AddFromMagnetLink(RemoteEpisode remoteEpisode, string hash, string magnetLink)
{
_proxy.AddTorrentFromUrl(magnetLink, GetDownloadDirectory(), Settings);
_proxy.SetTorrentSeedingConfiguration(hash, remoteEpisode.SeedConfiguration, Settings);
var isRecentEpisode = remoteEpisode.IsRecentEpisode();
@@ -144,6 +149,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
protected override string AddFromTorrentFile(RemoteEpisode remoteEpisode, string hash, string filename, byte[] fileContent)
{
_proxy.AddTorrentFromData(fileContent, GetDownloadDirectory(), Settings);
_proxy.SetTorrentSeedingConfiguration(hash, remoteEpisode.SeedConfiguration, Settings);
var isRecentEpisode = remoteEpisode.IsRecentEpisode();
@@ -174,17 +180,13 @@ namespace NzbDrone.Core.Download.Clients.Transmission
{
return Settings.TvDirectory;
}
else if (Settings.TvCategory.IsNotNullOrWhiteSpace())
{
var config = _proxy.GetConfig(Settings);
var destDir = (string)config.GetValueOrDefault("download-dir");
return string.Format("{0}/{1}", destDir.TrimEnd('/'), Settings.TvCategory);
}
else
{
return null;
}
if (!Settings.TvCategory.IsNotNullOrWhiteSpace()) return null;
var config = _proxy.GetConfig(Settings);
var destDir = (string)config.GetValueOrDefault("download-dir");
return $"{destDir.TrimEnd('/')}/{Settings.TvCategory}";
}
protected ValidationFailure TestConnection()

View File

@@ -37,7 +37,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
_authSessionIDCache = cacheManager.GetCache<string>(GetType(), "authSessionID");
}
public List<TransmissionTorrent> GetTorrents(TransmissionSettings settings)
{
var result = GetTorrentStatus(settings);
@@ -77,8 +77,10 @@ namespace NzbDrone.Core.Download.Clients.Transmission
public void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, TransmissionSettings settings)
{
if (seedConfiguration == null) return;
var arguments = new Dictionary<string, object>();
arguments.Add("ids", new string[] { hash });
arguments.Add("ids", new[] { hash });
if (seedConfiguration.Ratio != null)
{
@@ -167,9 +169,12 @@ namespace NzbDrone.Core.Download.Clients.Transmission
"leftUntilDone",
"isFinished",
"eta",
"errorString"
"errorString",
"uploadedEver",
"downloadedEver",
"seedRatioLimit"
};
var arguments = new Dictionary<string, object>();
arguments.Add("fields", fields);
@@ -237,7 +242,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
requestBuilder.SetHeader("X-Transmission-Session-Id", sessionId);
}
public TransmissionResponse ProcessRequest(string action, object arguments, TransmissionSettings settings)
{
try

View File

@@ -23,5 +23,11 @@
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; }
}
}

View File

@@ -104,6 +104,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
item.TotalSize = torrent.TotalSize;
item.RemainingSize = torrent.RemainingSize;
item.Category = torrent.Category;
item.SeedRatio = torrent.Ratio;
if (torrent.DownRate > 0)
{

View File

@@ -40,6 +40,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
{
_proxy.AddTorrentFromUrl(magnetLink, Settings);
_proxy.SetTorrentLabel(hash, Settings.TvCategory, Settings);
_proxy.SetTorrentSeedingConfiguration(hash, remoteEpisode.SeedConfiguration, Settings);
var isRecentEpisode = remoteEpisode.IsRecentEpisode();
@@ -58,6 +59,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
{
_proxy.AddTorrentFromFile(filename, fileContent, Settings);
_proxy.SetTorrentLabel(hash, Settings.TvCategory, Settings);
_proxy.SetTorrentSeedingConfiguration(hash, remoteEpisode.SeedConfiguration, Settings);
var isRecentEpisode = remoteEpisode.IsRecentEpisode();
@@ -94,6 +96,8 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
item.Category = torrent.Label;
item.DownloadClient = Definition.Name;
item.RemainingSize = torrent.Remaining;
item.SeedRatio = torrent.Ratio;
if (torrent.Eta != -1)
{
item.RemainingTime = TimeSpan.FromSeconds(torrent.Eta);
@@ -101,7 +105,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
var outputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.RootDownloadPath));
if (outputPath == null || outputPath.FileName == torrent.Name)
if (outputPath.FileName == torrent.Name)
{
item.OutputPath = outputPath;
}
@@ -134,7 +138,9 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
}
// 'Started' without 'Queued' is when the torrent is 'forced seeding'
item.CanMoveFiles = item.CanBeRemoved = (!torrent.Status.HasFlag(UTorrentTorrentStatus.Queued) && !torrent.Status.HasFlag(UTorrentTorrentStatus.Started));
item.CanMoveFiles = item.CanBeRemoved =
!torrent.Status.HasFlag(UTorrentTorrentStatus.Queued) &&
!torrent.Status.HasFlag(UTorrentTorrentStatus.Started);
queueItems.Add(item);
}

View File

@@ -69,14 +69,14 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
return configuration;
}
public UTorrentResponse GetTorrents(string cacheID, UTorrentSettings settings)
public UTorrentResponse GetTorrents(string cacheId, UTorrentSettings settings)
{
var requestBuilder = BuildRequest(settings)
.AddQueryParam("list", 1);
if (cacheID.IsNotNullOrWhiteSpace())
if (cacheId.IsNotNullOrWhiteSpace())
{
requestBuilder.AddQueryParam("cid", cacheID);
requestBuilder.AddQueryParam("cid", cacheId);
}
var result = ProcessRequest(requestBuilder, settings);
@@ -99,17 +99,19 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
.Post()
.AddQueryParam("action", "add-file")
.AddQueryParam("path", string.Empty)
.AddFormUpload("torrent_file", fileName, fileContent, @"application/octet-stream");
.AddFormUpload("torrent_file", fileName, fileContent);
ProcessRequest(requestBuilder, settings);
}
public void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, UTorrentSettings settings)
{
if (seedConfiguration == null) return;
var requestBuilder = BuildRequest(settings)
.AddQueryParam("action", "setprops")
.AddQueryParam("hash", hash);
requestBuilder.AddQueryParam("s", "seed_override")
.AddQueryParam("v", 1);

View File

@@ -15,6 +15,7 @@ namespace NzbDrone.Core.Download
public long TotalSize { get; set; }
public long RemainingSize { get; set; }
public TimeSpan? RemainingTime { get; set; }
public double? SeedRatio { get; set; }
public OsPath OutputPath { get; set; }
public string Message { get; set; }

View File

@@ -5,6 +5,7 @@ using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Common.TPL;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download.Clients;
using NzbDrone.Core.Exceptions;
using NzbDrone.Core.Indexers;
@@ -26,6 +27,7 @@ namespace NzbDrone.Core.Download
private readonly IIndexerStatusService _indexerStatusService;
private readonly IRateLimitService _rateLimitService;
private readonly IEventAggregator _eventAggregator;
private readonly ISeedConfigProvider _seedConfigProvider;
private readonly Logger _logger;
public DownloadService(IProvideDownloadClient downloadClientProvider,
@@ -33,6 +35,7 @@ namespace NzbDrone.Core.Download
IIndexerStatusService indexerStatusService,
IRateLimitService rateLimitService,
IEventAggregator eventAggregator,
ISeedConfigProvider seedConfigProvider,
Logger logger)
{
_downloadClientProvider = downloadClientProvider;
@@ -40,6 +43,7 @@ namespace NzbDrone.Core.Download
_indexerStatusService = indexerStatusService;
_rateLimitService = rateLimitService;
_eventAggregator = eventAggregator;
_seedConfigProvider = seedConfigProvider;
_logger = logger;
}
@@ -56,6 +60,9 @@ namespace NzbDrone.Core.Download
throw new DownloadClientUnavailableException($"{remoteEpisode.Release.DownloadProtocol} Download client isn't configured yet");
}
// Get the seed configuration for this release.
remoteEpisode.SeedConfiguration = _seedConfigProvider.GetSeedConfiguration(remoteEpisode);
// Limit grabs to 2 per second.
if (remoteEpisode.Release.DownloadUrl.IsNotNullOrWhiteSpace() && !remoteEpisode.Release.DownloadUrl.StartsWith("magnet:"))
{

View File

@@ -1,6 +1,7 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using Marr.Data;
using NLog;
using NzbDrone.Common.Crypto;
using NzbDrone.Common.Extensions;
@@ -66,69 +67,74 @@ namespace NzbDrone.Core.Download.Pending
_logger = logger;
}
public void Add(DownloadDecision decision, PendingReleaseReason reason)
{
var alreadyPending = _repository.AllBySeriesId(decision.RemoteEpisode.Series.Id);
alreadyPending = IncludeRemoteEpisodes(alreadyPending);
Add(alreadyPending, decision, reason);
AddMany(new List<Tuple<DownloadDecision, PendingReleaseReason>> { Tuple.Create(decision, reason) });
}
public void AddMany(List<Tuple<DownloadDecision, PendingReleaseReason>> decisions)
{
var alreadyPending = decisions.Select(v => v.Item1.RemoteEpisode.Series.Id).Distinct().SelectMany(_repository.AllBySeriesId).ToList();
alreadyPending = IncludeRemoteEpisodes(alreadyPending);
foreach (var pair in decisions)
foreach (var seriesDecisions in decisions.GroupBy(v => v.Item1.RemoteEpisode.Series.Id))
{
Add(alreadyPending, pair.Item1, pair.Item2);
var series = seriesDecisions.First().Item1.RemoteEpisode.Series;
var alreadyPending = _repository.AllBySeriesId(series.Id);
alreadyPending = IncludeRemoteEpisodes(alreadyPending, seriesDecisions.ToDictionaryIgnoreDuplicates(v => v.Item1.RemoteEpisode.Release.Title, v => v.Item1.RemoteEpisode));
var alreadyPendingByEpisode = CreateEpisodeLookup(alreadyPending);
foreach (var pair in seriesDecisions)
{
var decision = pair.Item1;
var reason = pair.Item2;
var episodeIds = decision.RemoteEpisode.Episodes.Select(e => e.Id);
var existingReports = episodeIds.SelectMany(v => alreadyPendingByEpisode[v] ?? Enumerable.Empty<PendingRelease>())
.Distinct().ToList();
var matchingReports = existingReports.Where(MatchingReleasePredicate(decision.RemoteEpisode.Release)).ToList();
if (matchingReports.Any())
{
var matchingReport = matchingReports.First();
if (matchingReport.Reason != reason)
{
_logger.Debug("The release {0} is already pending with reason {1}, changing to {2}", decision.RemoteEpisode, matchingReport.Reason, reason);
matchingReport.Reason = reason;
_repository.Update(matchingReport);
}
else
{
_logger.Debug("The release {0} is already pending with reason {1}, not adding again", decision.RemoteEpisode, reason);
}
if (matchingReports.Count() > 1)
{
_logger.Debug("The release {0} had {1} duplicate pending, removing duplicates.", decision.RemoteEpisode, matchingReports.Count() - 1);
foreach (var duplicate in matchingReports.Skip(1))
{
_repository.Delete(duplicate.Id);
alreadyPending.Remove(duplicate);
alreadyPendingByEpisode = CreateEpisodeLookup(alreadyPending);
}
}
continue;
}
_logger.Debug("Adding release {0} to pending releases with reason {1}", decision.RemoteEpisode, reason);
Insert(decision, reason);
}
}
}
private void Add(List<PendingRelease> alreadyPending, DownloadDecision decision, PendingReleaseReason reason)
private ILookup<int, PendingRelease> CreateEpisodeLookup(IEnumerable<PendingRelease> alreadyPending)
{
var episodeIds = decision.RemoteEpisode.Episodes.Select(e => e.Id);
var existingReports = alreadyPending.Where(r => r.RemoteEpisode.Episodes.Select(e => e.Id)
.Intersect(episodeIds)
.Any());
var matchingReports = existingReports.Where(MatchingReleasePredicate(decision.RemoteEpisode.Release)).ToList();
if (matchingReports.Any())
{
var matchingReport = matchingReports.First();
if (matchingReport.Reason != reason)
{
_logger.Debug("The release {0} is already pending with reason {1}, changing to {2}", decision.RemoteEpisode, matchingReport.Reason, reason);
matchingReport.Reason = reason;
_repository.Update(matchingReport);
}
else
{
_logger.Debug("The release {0} is already pending with reason {1}, not adding again", decision.RemoteEpisode, reason);
}
if (matchingReports.Count() > 1)
{
_logger.Debug("The release {0} had {1} duplicate pending, removing duplicates.", decision.RemoteEpisode, matchingReports.Count() - 1);
foreach (var duplicate in matchingReports.Skip(1))
{
_repository.Delete(duplicate.Id);
alreadyPending.Remove(duplicate);
}
}
return;
}
_logger.Debug("Adding release {0} to pending releases with reason {1}", decision.RemoteEpisode, reason);
Insert(decision, reason);
return alreadyPending.SelectMany(v => v.RemoteEpisode.Episodes
.Select(d => new { Episode = d, PendingRelease = v }))
.ToLookup(v => v.Episode.Id, v => v.PendingRelease);
}
public List<ReleaseInfo> GetPending()
@@ -254,11 +260,27 @@ namespace NzbDrone.Core.Download.Pending
return IncludeRemoteEpisodes(_repository.AllBySeriesId(seriesId).ToList());
}
private List<PendingRelease> IncludeRemoteEpisodes(List<PendingRelease> releases)
private List<PendingRelease> IncludeRemoteEpisodes(List<PendingRelease> releases, Dictionary<string, RemoteEpisode> knownRemoteEpisodes = null)
{
var result = new List<PendingRelease>();
var seriesMap = _seriesService.GetSeries(releases.Select(v => v.SeriesId).Distinct())
.ToDictionary(v => v.Id);
var seriesMap = new Dictionary<int, Series>();
if (knownRemoteEpisodes != null)
{
foreach (var series in knownRemoteEpisodes.Values.Select(v => v.Series))
{
if (!seriesMap.ContainsKey(series.Id))
{
seriesMap[series.Id] = series;
}
}
}
foreach (var series in _seriesService.GetSeries(releases.Select(v => v.SeriesId).Distinct().Where(v => !seriesMap.ContainsKey(v))))
{
seriesMap[series.Id] = series;
}
foreach (var release in releases)
{
@@ -267,7 +289,24 @@ namespace NzbDrone.Core.Download.Pending
// Just in case the series was removed, but wasn't cleaned up yet (housekeeper will clean it up)
if (series == null) return null;
var episodes = _parsingService.GetEpisodes(release.ParsedEpisodeInfo, series, true);
List<Episode> episodes;
RemoteEpisode knownRemoteEpisode;
if (knownRemoteEpisodes != null && knownRemoteEpisodes.TryGetValue(release.Release.Title, out knownRemoteEpisode))
{
episodes = knownRemoteEpisode.Episodes;
}
else
{
if (ValidateParsedEpisodeInfo.ValidateForSeriesType(release.ParsedEpisodeInfo, series))
{
episodes = _parsingService.GetEpisodes(release.ParsedEpisodeInfo, series, true);
}
else
{
episodes = new List<Episode>();
}
}
release.RemoteEpisode = new RemoteEpisode
{

View File

@@ -40,9 +40,10 @@ namespace NzbDrone.Core.Download
var prioritizedDecisions = _prioritizeDownloadDecision.PrioritizeDecisions(qualifiedReports);
var grabbed = new List<DownloadDecision>();
var pending = new List<DownloadDecision>();
var failed = new List<DownloadDecision>();
var rejected = decisions.Where(d => d.Rejected).ToList();
var pendingAddQueue = new List<Tuple<DownloadDecision, PendingReleaseReason>>();
var usenetFailed = false;
var torrentFailed = false;
@@ -59,15 +60,14 @@ namespace NzbDrone.Core.Download
if (report.TemporarilyRejected)
{
_pendingReleaseService.Add(report, PendingReleaseReason.Delay);
pending.Add(report);
PreparePending(pendingAddQueue, grabbed, pending, report, PendingReleaseReason.Delay);
continue;
}
if (downloadProtocol == DownloadProtocol.Usenet && usenetFailed ||
downloadProtocol == DownloadProtocol.Torrent && torrentFailed)
{
failed.Add(report);
PreparePending(pendingAddQueue, grabbed, pending, report, PendingReleaseReason.DownloadClientUnavailable);
continue;
}
@@ -86,7 +86,7 @@ namespace NzbDrone.Core.Download
if (ex is DownloadClientUnavailableException || ex is DownloadClientAuthenticationException)
{
_logger.Debug(ex, "Failed to send release to download client, storing until later. " + remoteEpisode);
failed.Add(report);
PreparePending(pendingAddQueue, grabbed, pending, report, PendingReleaseReason.DownloadClientUnavailable);
if (downloadProtocol == DownloadProtocol.Usenet)
{
@@ -104,7 +104,10 @@ namespace NzbDrone.Core.Download
}
}
pending.AddRange(ProcessFailedGrabs(grabbed, failed));
if (pendingAddQueue.Any())
{
_pendingReleaseService.AddMany(pendingAddQueue);
}
return new ProcessedDecisions(grabbed, pending, rejected);
}
@@ -126,45 +129,22 @@ namespace NzbDrone.Core.Download
.Any();
}
private List<DownloadDecision> ProcessFailedGrabs(List<DownloadDecision> grabbed, List<DownloadDecision> failed)
private void PreparePending(List<Tuple<DownloadDecision, PendingReleaseReason>> queue, List<DownloadDecision> grabbed, List<DownloadDecision> pending, DownloadDecision report, PendingReleaseReason reason)
{
var pending = new List<DownloadDecision>();
var stored = new List<DownloadDecision>();
// If a release was already grabbed with matching episodes we should store it as a fallback
// and filter it out the next time it is processed.
// If a higher quality release failed to add to the download client, but a lower quality release
// was sent to another client we still list it normally so it apparent that it'll grab next time.
// Delayed is treated the same, but only the first is listed the subsequent items as stored as Fallback.
var addQueue = new List<Tuple<DownloadDecision, PendingReleaseReason>>();
foreach (var report in failed)
if (IsEpisodeProcessed(grabbed, report) ||
IsEpisodeProcessed(pending, report))
{
// If a release was already grabbed with matching episodes we should store it as a fallback
// and filter it out the next time it is processed incase a higher quality release failed to
// add to the download client, but a lower quality release was sent to another client
// If the release wasn't grabbed already, but was already stored, store it as a fallback,
// otherwise store it as DownloadClientUnavailable.
if (IsEpisodeProcessed(grabbed, report))
{
addQueue.Add(Tuple.Create(report, PendingReleaseReason.Fallback));
pending.Add(report);
}
else if (IsEpisodeProcessed(stored, report))
{
addQueue.Add(Tuple.Create(report, PendingReleaseReason.Fallback));
pending.Add(report);
}
else
{
addQueue.Add(Tuple.Create(report, PendingReleaseReason.DownloadClientUnavailable));
pending.Add(report);
stored.Add(report);
}
reason = PendingReleaseReason.Fallback;
}
if (addQueue.Any())
{
_pendingReleaseService.AddMany(addQueue);
}
return pending;
queue.Add(Tuple.Create(report, reason));
pending.Add(report);
}
}
}

View File

@@ -18,7 +18,7 @@ namespace NzbDrone.Core.Extras
{
public interface IExtraService
{
void ImportExtraFiles(LocalEpisode localEpisode, EpisodeFile episodeFile, bool isReadOnly);
void ImportEpisode(LocalEpisode localEpisode, EpisodeFile episodeFile, bool isReadOnly);
}
public class ExtraService : IExtraService,
@@ -48,15 +48,15 @@ namespace NzbDrone.Core.Extras
_logger = logger;
}
public void ImportExtraFiles(LocalEpisode localEpisode, EpisodeFile episodeFile, bool isReadOnly)
public void ImportEpisode(LocalEpisode localEpisode, EpisodeFile episodeFile, bool isReadOnly)
{
var series = localEpisode.Series;
foreach (var extraFileManager in _extraFileManagers)
{
extraFileManager.CreateAfterEpisodeImport(series, episodeFile);
}
ImportExtraFiles(localEpisode, episodeFile, isReadOnly);
CreateAfterImport(localEpisode.Series, episodeFile);
}
private void ImportExtraFiles(LocalEpisode localEpisode, EpisodeFile episodeFile, bool isReadOnly)
{
if (!_configService.ImportExtraFiles)
{
return;
@@ -87,7 +87,7 @@ namespace NzbDrone.Core.Extras
foreach (var extraFileManager in _extraFileManagers)
{
var extension = Path.GetExtension(matchingFilename);
var extraFile = extraFileManager.Import(series, episodeFile, matchingFilename, extension, isReadOnly);
var extraFile = extraFileManager.Import(localEpisode.Series, episodeFile, matchingFilename, extension, isReadOnly);
if (extraFile != null)
{
@@ -102,6 +102,14 @@ namespace NzbDrone.Core.Extras
}
}
private void CreateAfterImport(Series series, EpisodeFile episodeFile)
{
foreach (var extraFileManager in _extraFileManagers)
{
extraFileManager.CreateAfterEpisodeImport(series, episodeFile);
}
}
public void Handle(MediaCoversUpdatedEvent message)
{
var series = message.Series;

View File

@@ -49,8 +49,6 @@ namespace NzbDrone.Core.Extras.Files
_logger = logger;
}
public virtual bool PermanentlyDelete => false;
public List<TExtraFile> GetFilesBySeries(int seriesId)
{
return _repository.GetFilesBySeries(seriesId);
@@ -122,17 +120,9 @@ namespace NzbDrone.Core.Extras.Files
if (_diskProvider.FileExists(path))
{
if (PermanentlyDelete)
{
_diskProvider.DeleteFile(path);
}
else
{
// Send extra files to the recycling bin so they can be recovered if necessary
var subfolder = _diskProvider.GetParentFolder(series.Path).GetRelativePath(_diskProvider.GetParentFolder(path));
_recycleBinProvider.DeleteFile(path, subfolder);
}
// Send to the recycling bin so they can be recovered if necessary
var subfolder = _diskProvider.GetParentFolder(series.Path).GetRelativePath(_diskProvider.GetParentFolder(path));
_recycleBinProvider.DeleteFile(path, subfolder);
}
}
}

View File

@@ -7,6 +7,7 @@ using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Linq;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Extras.Metadata.Files;
using NzbDrone.Core.MediaCover;
@@ -18,14 +19,20 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
{
public class XbmcMetadata : MetadataBase<XbmcMetadataSettings>
{
private readonly IMapCoversToLocal _mediaCoverService;
private readonly Logger _logger;
private readonly IMapCoversToLocal _mediaCoverService;
private readonly IDetectXbmcNfo _detectNfo;
private readonly IDiskProvider _diskProvider;
public XbmcMetadata(IMapCoversToLocal mediaCoverService,
public XbmcMetadata(IDetectXbmcNfo detectNfo,
IDiskProvider diskProvider,
IMapCoversToLocal mediaCoverService,
Logger logger)
{
_mediaCoverService = mediaCoverService;
_logger = logger;
_mediaCoverService = mediaCoverService;
_diskProvider = diskProvider;
_detectNfo = detectNfo;
}
private static readonly Regex SeriesImagesRegex = new Regex(@"^(?<type>poster|banner|fanart)\.(?:png|jpg)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
@@ -114,7 +121,8 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
if (parseResult != null &&
!parseResult.FullSeason &&
Path.GetExtension(filename).Equals(".nfo", StringComparison.OrdinalIgnoreCase))
Path.GetExtension(filename).Equals(".nfo", StringComparison.OrdinalIgnoreCase) &&
_detectNfo.IsXbmcNfoFile(path))
{
metadata.Type = MetadataType.EpisodeMetadata;
return metadata;
@@ -199,6 +207,8 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
_logger.Debug("Generating Episode Metadata for: {0}", Path.Combine(series.Path, episodeFile.RelativePath));
var watched = GetExistingWatchedStatus(series, episodeFile.RelativePath);
var xmlResult = string.Empty;
foreach (var episode in episodeFile.Episodes.Value)
{
@@ -233,7 +243,7 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
details.Add(new XElement("thumb", image.Url));
}
details.Add(new XElement("watched", "false"));
details.Add(new XElement("watched", watched));
if (episode.Ratings != null && episode.Ratings.Votes > 0)
{
@@ -382,5 +392,19 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
{
return Path.ChangeExtension(episodeFilePath, "").Trim('.') + "-thumb.jpg";
}
private bool GetExistingWatchedStatus(Series series, string episodeFilePath)
{
var fullPath = Path.Combine(series.Path, GetEpisodeMetadataFilename(episodeFilePath));
if (!_diskProvider.FileExists(fullPath))
{
return false;
}
var fileContent = _diskProvider.ReadAllText(fullPath);
return Regex.IsMatch(fileContent, "<watched>true</watched>");
}
}
}

View File

@@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using NzbDrone.Common.Disk;
namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
{
public interface IDetectXbmcNfo
{
bool IsXbmcNfoFile(string path);
}
public class XbmcNfoDetector : IDetectXbmcNfo
{
private readonly IDiskProvider _diskProvider;
private readonly Regex _regex = new Regex("<(movie|tvshow|episodedetails|artist|album|musicvideo)>", RegexOptions.Compiled);
public XbmcNfoDetector(IDiskProvider diskProvider)
{
_diskProvider = diskProvider;
}
public bool IsXbmcNfoFile(string path)
{
// Lets make sure we're not reading huge files.
if (_diskProvider.GetFileSize(path) > 10.Megabytes())
{
return false;
}
// Check if it contains some of the kodi/xbmc xml tags
var content = _diskProvider.ReadAllText(path);
return _regex.IsMatch(content);
}
}
}

View File

@@ -6,7 +6,9 @@ using NzbDrone.Common.Extensions;
using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.Extras.Metadata.Files;
using NzbDrone.Core.Extras.Subtitles;
using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Extras.Metadata
@@ -14,18 +16,18 @@ namespace NzbDrone.Core.Extras.Metadata
public class ExistingMetadataImporter : ImportExistingExtraFilesBase<MetadataFile>
{
private readonly IExtraFileService<MetadataFile> _metadataFileService;
private readonly IParsingService _parsingService;
private readonly IAugmentingService _augmentingService;
private readonly Logger _logger;
private readonly List<IMetadata> _consumers;
public ExistingMetadataImporter(IExtraFileService<MetadataFile> metadataFileService,
IEnumerable<IMetadata> consumers,
IParsingService parsingService,
IAugmentingService augmentingService,
Logger logger)
: base(metadataFileService)
{
_metadataFileService = metadataFileService;
_parsingService = parsingService;
_augmentingService = augmentingService;
_logger = logger;
_consumers = consumers.ToList();
}
@@ -60,9 +62,18 @@ namespace NzbDrone.Core.Extras.Metadata
if (metadata.Type == MetadataType.EpisodeImage ||
metadata.Type == MetadataType.EpisodeMetadata)
{
var localEpisode = _parsingService.GetLocalEpisode(possibleMetadataFile, series);
var localEpisode = new LocalEpisode
{
FileEpisodeInfo = Parser.Parser.ParsePath(possibleMetadataFile),
Series = series,
Path = possibleMetadataFile
};
if (localEpisode == null)
try
{
_augmentingService.Augment(localEpisode, false);
}
catch (AugmentingFailedException ex)
{
_logger.Debug("Unable to parse extra file: {0}", possibleMetadataFile);
continue;

View File

@@ -16,7 +16,5 @@ namespace NzbDrone.Core.Extras.Metadata.Files
: base(repository, seriesService, diskProvider, recycleBinProvider, logger)
{
}
public override bool PermanentlyDelete => true;
}
}

View File

@@ -10,6 +10,7 @@ using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.Extras.Metadata.Files;
using NzbDrone.Core.Extras.Others;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Tv;
@@ -19,6 +20,8 @@ namespace NzbDrone.Core.Extras.Metadata
{
private readonly IMetadataFactory _metadataFactory;
private readonly ICleanMetadataService _cleanMetadataService;
private readonly IRecycleBinProvider _recycleBinProvider;
private readonly IOtherExtraFileRenamer _otherExtraFileRenamer;
private readonly IDiskTransferService _diskTransferService;
private readonly IDiskProvider _diskProvider;
private readonly IHttpClient _httpClient;
@@ -29,6 +32,8 @@ namespace NzbDrone.Core.Extras.Metadata
public MetadataService(IConfigService configService,
IDiskProvider diskProvider,
IDiskTransferService diskTransferService,
IRecycleBinProvider recycleBinProvider,
IOtherExtraFileRenamer otherExtraFileRenamer,
IMetadataFactory metadataFactory,
ICleanMetadataService cleanMetadataService,
IHttpClient httpClient,
@@ -39,6 +44,8 @@ namespace NzbDrone.Core.Extras.Metadata
{
_metadataFactory = metadataFactory;
_cleanMetadataService = cleanMetadataService;
_otherExtraFileRenamer = otherExtraFileRenamer;
_recycleBinProvider = recycleBinProvider;
_diskTransferService = diskTransferService;
_diskProvider = diskProvider;
_httpClient = httpClient;
@@ -88,7 +95,6 @@ namespace NzbDrone.Core.Extras.Metadata
foreach (var consumer in _metadataFactory.Enabled())
{
files.AddIfNotNull(ProcessEpisodeMetadata(consumer, series, episodeFile, new List<MetadataFile>()));
files.AddRange(ProcessEpisodeImages(consumer, series, episodeFile, new List<MetadataFile>()));
}
@@ -235,6 +241,8 @@ namespace NzbDrone.Core.Extras.Metadata
var fullPath = Path.Combine(series.Path, episodeMetadata.RelativePath);
_otherExtraFileRenamer.RenameOtherExtraFile(series, fullPath);
var existingMetadata = GetMetadataFile(series, existingMetadataFiles, c => c.Type == MetadataType.EpisodeMetadata &&
c.EpisodeFileId == episodeFile.Id);
@@ -289,6 +297,8 @@ namespace NzbDrone.Core.Extras.Metadata
continue;
}
_otherExtraFileRenamer.RenameOtherExtraFile(series, fullPath);
var metadata = GetMetadataFile(series, existingMetadataFiles, c => c.Type == MetadataType.SeriesImage &&
c.RelativePath == image.RelativePath) ??
new MetadataFile
@@ -324,6 +334,8 @@ namespace NzbDrone.Core.Extras.Metadata
continue;
}
_otherExtraFileRenamer.RenameOtherExtraFile(series, fullPath);
var metadata = GetMetadataFile(series, existingMetadataFiles, c => c.Type == MetadataType.SeasonImage &&
c.SeasonNumber == season.SeasonNumber &&
c.RelativePath == image.RelativePath) ??
@@ -360,6 +372,8 @@ namespace NzbDrone.Core.Extras.Metadata
continue;
}
_otherExtraFileRenamer.RenameOtherExtraFile(series, fullPath);
var existingMetadata = GetMetadataFile(series, existingMetadataFiles, c => c.Type == MetadataType.EpisodeImage &&
c.EpisodeFileId == episodeFile.Id);
@@ -443,11 +457,11 @@ namespace NzbDrone.Core.Extras.Metadata
_logger.Debug("Removing duplicate Metadata file: {0}", path);
_diskProvider.DeleteFile(path);
var subfolder = _diskProvider.GetParentFolder(series.Path).GetRelativePath(_diskProvider.GetParentFolder(path));
_recycleBinProvider.DeleteFile(path, subfolder);
_metadataFileService.Delete(file.Id);
}
return matchingMetadataFiles.First();
}
}

View File

@@ -1,10 +1,13 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Extras.Others
@@ -12,16 +15,16 @@ namespace NzbDrone.Core.Extras.Others
public class ExistingOtherExtraImporter : ImportExistingExtraFilesBase<OtherExtraFile>
{
private readonly IExtraFileService<OtherExtraFile> _otherExtraFileService;
private readonly IParsingService _parsingService;
private readonly IAugmentingService _augmentingService;
private readonly Logger _logger;
public ExistingOtherExtraImporter(IExtraFileService<OtherExtraFile> otherExtraFileService,
IParsingService parsingService,
IAugmentingService augmentingService,
Logger logger)
: base(otherExtraFileService)
{
_otherExtraFileService = otherExtraFileService;
_parsingService = parsingService;
_augmentingService = augmentingService;
_logger = logger;
}
@@ -44,9 +47,18 @@ namespace NzbDrone.Core.Extras.Others
continue;
}
var localEpisode = _parsingService.GetLocalEpisode(possibleExtraFile, series);
var localEpisode = new LocalEpisode
{
FileEpisodeInfo = Parser.Parser.ParsePath(possibleExtraFile),
Series = series,
Path = possibleExtraFile
};
if (localEpisode == null)
try
{
_augmentingService.Augment(localEpisode, false);
}
catch (AugmentingFailedException ex)
{
_logger.Debug("Unable to parse extra file: {0}", possibleExtraFile);
continue;

View File

@@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Extras.Others
{
public interface IOtherExtraFileRenamer
{
void RenameOtherExtraFile(Series series, string path);
}
public class OtherExtraFileRenamer : IOtherExtraFileRenamer
{
private readonly Logger _logger;
private readonly IDiskProvider _diskProvider;
private readonly IRecycleBinProvider _recycleBinProvider;
private readonly ISeriesService _seriesService;
private readonly IOtherExtraFileService _otherExtraFileService;
public OtherExtraFileRenamer(IOtherExtraFileService otherExtraFileService,
ISeriesService seriesService,
IRecycleBinProvider recycleBinProvider,
IDiskProvider diskProvider,
Logger logger)
{
_logger = logger;
_diskProvider = diskProvider;
_recycleBinProvider = recycleBinProvider;
_seriesService = seriesService;
_otherExtraFileService = otherExtraFileService;
}
public void RenameOtherExtraFile(Series series, string path)
{
if (!_diskProvider.FileExists(path))
{
return;
}
var relativePath = series.Path.GetRelativePath(path);
var otherExtraFile = _otherExtraFileService.FindByPath(relativePath);
if (otherExtraFile != null)
{
var newPath = path + "-orig";
// Recycle an existing -orig file.
RemoveOtherExtraFile(series, newPath);
// Rename the file to .*-orig
_diskProvider.MoveFile(path, newPath);
otherExtraFile.RelativePath = relativePath + "-orig";
otherExtraFile.Extension += "-orig";
_otherExtraFileService.Upsert(otherExtraFile);
}
}
private void RemoveOtherExtraFile(Series series, string path)
{
if (!_diskProvider.FileExists(path))
{
return;
}
var relativePath = series.Path.GetRelativePath(path);
var otherExtraFile = _otherExtraFileService.FindByPath(relativePath);
if (otherExtraFile != null)
{
var subfolder = Path.GetDirectoryName(relativePath);
_recycleBinProvider.DeleteFile(path, subfolder);
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More