mirror of https://github.com/lidarr/Lidarr
New: Nested Settings and Seed Ratio Setting (#379)
* New: Nested Settings and Seed Ratio Setting * Fixed: Sonarr related variable naming
This commit is contained in:
parent
dabb9bc18a
commit
089d213816
|
@ -1,4 +1,4 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FluentValidation;
|
||||
using Nancy;
|
||||
|
@ -8,6 +8,9 @@ using NzbDrone.Core.DecisionEngine;
|
|||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using Lidarr.Http.Extensions;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Indexers;
|
||||
|
||||
namespace Lidarr.Api.V1.Indexers
|
||||
{
|
||||
|
@ -15,14 +18,17 @@ namespace Lidarr.Api.V1.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 Lidarr.Api.V1.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -171,7 +171,12 @@ namespace Lidarr.Api.V1
|
|||
|
||||
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))
|
||||
{
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Lidarr.Http.ClientSchema
|
||||
{
|
||||
|
@ -7,11 +7,17 @@ namespace Lidarr.Http.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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Lidarr.Http.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; }
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
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 Lidarr.Http.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);
|
||||
|
||||
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);
|
||||
}
|
||||
var field = mapping.Field.Clone();
|
||||
field.Value = mapping.GetterFunc(model);
|
||||
|
||||
result.Add(field);
|
||||
}
|
||||
}
|
||||
|
||||
return result.OrderBy(r => r.Order).ToList();
|
||||
|
@ -59,81 +37,15 @@ namespace Lidarr.Http.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 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;
|
||||
|
@ -142,15 +54,160 @@ namespace Lidarr.Http.ClientSchema
|
|||
|
||||
public static T ReadFromSchema<T>(List<Field> fields)
|
||||
{
|
||||
return (T)ReadFromSchema(fields, typeof(T));
|
||||
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)
|
||||
select new SelectOption { Value = Convert.ToInt32(e), Name = e.ToString() };
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -65,6 +65,7 @@
|
|||
<Reference Include="System.Data" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="ClientSchema\FieldMapping.cs" />
|
||||
<Compile Include="Exceptions\ApiException.cs" />
|
||||
<Compile Include="Authentication\AuthenticationModule.cs" />
|
||||
<Compile Include="Authentication\AuthenticationService.cs" />
|
||||
|
|
|
@ -21,20 +21,38 @@ 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
|
||||
{
|
||||
[FieldDefinition(0, Label = "First Name", HelpText = "Your First Name")]
|
||||
|
@ -45,4 +63,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; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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" }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 RemoteAlbum
|
||||
{
|
||||
Release = new ReleaseInfo
|
||||
{
|
||||
DownloadProtocol = DownloadProtocol.Torrent,
|
||||
IndexerId = 0
|
||||
}
|
||||
});
|
||||
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_discography_time_for_discography_packs()
|
||||
{
|
||||
var settings = new TorznabSettings();
|
||||
settings.SeedCriteria.DiscographySeedTime = 10;
|
||||
|
||||
Mocker.GetMock<IIndexerFactory>()
|
||||
.Setup(v => v.Get(It.IsAny<int>()))
|
||||
.Returns(new IndexerDefinition
|
||||
{
|
||||
Settings = settings
|
||||
});
|
||||
|
||||
var result = Subject.GetSeedConfiguration(new RemoteAlbum
|
||||
{
|
||||
Release = new ReleaseInfo()
|
||||
{
|
||||
DownloadProtocol = DownloadProtocol.Torrent,
|
||||
IndexerId = 1
|
||||
},
|
||||
ParsedAlbumInfo = new ParsedAlbumInfo
|
||||
{
|
||||
Discography = true
|
||||
}
|
||||
});
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result.SeedTime.Should().Be(TimeSpan.FromMinutes(10));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -253,6 +253,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" />
|
||||
|
|
|
@ -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; }
|
||||
|
|
|
@ -45,6 +45,8 @@ namespace NzbDrone.Core.Download.Clients.Deluge
|
|||
_proxy.SetLabel(actualHash, Settings.MusicCategory, Settings);
|
||||
}
|
||||
|
||||
_proxy.SetTorrentSeedingConfiguration(actualHash, remoteAlbum.SeedConfiguration, Settings);
|
||||
|
||||
var isRecentAlbum = remoteAlbum.IsRecentAlbum();
|
||||
|
||||
if (isRecentAlbum && 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, remoteAlbum.SeedConfiguration, Settings);
|
||||
|
||||
if (!Settings.MusicCategory.IsNullOrWhiteSpace())
|
||||
{
|
||||
_proxy.SetLabel(actualHash, Settings.MusicCategory, 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
|
||||
{
|
||||
item.RemainingTime = TimeSpan.FromSeconds(torrent.Eta);
|
||||
|
@ -144,8 +149,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);
|
||||
}
|
||||
|
|
|
@ -150,13 +150,20 @@ 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)
|
||||
|
@ -175,7 +182,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);
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.Deluge
|
||||
{
|
||||
|
@ -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")]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -147,7 +147,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
|
|||
{
|
||||
_logger.Debug(e, "Failed to get config from Download Station");
|
||||
|
||||
throw e;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -140,6 +140,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]),
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
namespace NzbDrone.Core.Download.Clients.Hadouken.Models
|
||||
namespace NzbDrone.Core.Download.Clients.Hadouken.Models
|
||||
{
|
||||
public sealed class HadoukenTorrent
|
||||
{
|
||||
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -40,10 +40,10 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||
_proxy.SetTorrentLabel(hash.ToLower(), Settings.MusicCategory, Settings);
|
||||
}
|
||||
|
||||
var isRecentEpisode = remoteAlbum.IsRecentAlbum();
|
||||
var isRecentAlbum = remoteAlbum.IsRecentAlbum();
|
||||
|
||||
if (isRecentEpisode && Settings.RecentTvPriority == (int)QBittorrentPriority.First ||
|
||||
!isRecentEpisode && Settings.OlderTvPriority == (int)QBittorrentPriority.First)
|
||||
if (isRecentAlbum && Settings.RecentTvPriority == (int)QBittorrentPriority.First ||
|
||||
!isRecentAlbum && Settings.OlderTvPriority == (int)QBittorrentPriority.First)
|
||||
{
|
||||
_proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings);
|
||||
}
|
||||
|
@ -108,6 +108,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||
item.DownloadClient = Definition.Name;
|
||||
item.RemainingSize = (long)(torrent.Size * (1.0 - torrent.Progress));
|
||||
item.RemainingTime = GetRemainingTime(torrent);
|
||||
item.SeedRatio = torrent.Ratio;
|
||||
|
||||
item.OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.SavePath));
|
||||
|
||||
|
|
|
@ -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,11 +133,12 @@ namespace NzbDrone.Core.Download.Clients.Transmission
|
|||
protected override string AddFromMagnetLink(RemoteAlbum remoteAlbum, string hash, string magnetLink)
|
||||
{
|
||||
_proxy.AddTorrentFromUrl(magnetLink, GetDownloadDirectory(), Settings);
|
||||
_proxy.SetTorrentSeedingConfiguration(hash, remoteAlbum.SeedConfiguration, Settings);
|
||||
|
||||
var isRecentEpisode = remoteAlbum.IsRecentAlbum();
|
||||
var isRecentAlbum = remoteAlbum.IsRecentAlbum();
|
||||
|
||||
if (isRecentEpisode && Settings.RecentTvPriority == (int)TransmissionPriority.First ||
|
||||
!isRecentEpisode && Settings.OlderTvPriority == (int)TransmissionPriority.First)
|
||||
if (isRecentAlbum && Settings.RecentTvPriority == (int)TransmissionPriority.First ||
|
||||
!isRecentAlbum && Settings.OlderTvPriority == (int)TransmissionPriority.First)
|
||||
{
|
||||
_proxy.MoveTorrentToTopInQueue(hash, Settings);
|
||||
}
|
||||
|
@ -144,11 +149,12 @@ namespace NzbDrone.Core.Download.Clients.Transmission
|
|||
protected override string AddFromTorrentFile(RemoteAlbum remoteAlbum, string hash, string filename, byte[] fileContent)
|
||||
{
|
||||
_proxy.AddTorrentFromData(fileContent, GetDownloadDirectory(), Settings);
|
||||
_proxy.SetTorrentSeedingConfiguration(hash, remoteAlbum.SeedConfiguration, Settings);
|
||||
|
||||
var isRecentEpisode = remoteAlbum.IsRecentAlbum();
|
||||
var isRecentAlbum = remoteAlbum.IsRecentAlbum();
|
||||
|
||||
if (isRecentEpisode && Settings.RecentTvPriority == (int)TransmissionPriority.First ||
|
||||
!isRecentEpisode && Settings.OlderTvPriority == (int)TransmissionPriority.First)
|
||||
if (isRecentAlbum && Settings.RecentTvPriority == (int)TransmissionPriority.First ||
|
||||
!isRecentAlbum && Settings.OlderTvPriority == (int)TransmissionPriority.First)
|
||||
{
|
||||
_proxy.MoveTorrentToTopInQueue(hash, Settings);
|
||||
}
|
||||
|
@ -174,17 +180,16 @@ namespace NzbDrone.Core.Download.Clients.Transmission
|
|||
{
|
||||
return Settings.TvDirectory;
|
||||
}
|
||||
else if (Settings.MusicCategory.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
var config = _proxy.GetConfig(Settings);
|
||||
var destDir = (string)config.GetValueOrDefault("download-dir");
|
||||
|
||||
return string.Format("{0}/{1}", destDir.TrimEnd('/'), Settings.MusicCategory);
|
||||
}
|
||||
else
|
||||
if (!Settings.MusicCategory.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var config = _proxy.GetConfig(Settings);
|
||||
var destDir = (string)config.GetValueOrDefault("download-dir");
|
||||
|
||||
return $"{destDir.TrimEnd('/')}/{Settings.MusicCategory}";
|
||||
}
|
||||
|
||||
protected ValidationFailure TestConnection()
|
||||
|
|
|
@ -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,7 +169,10 @@ namespace NzbDrone.Core.Download.Clients.Transmission
|
|||
"leftUntilDone",
|
||||
"isFinished",
|
||||
"eta",
|
||||
"errorString"
|
||||
"errorString",
|
||||
"uploadedEver",
|
||||
"downloadedEver",
|
||||
"seedRatioLimit"
|
||||
};
|
||||
|
||||
var arguments = new Dictionary<string, object>();
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
namespace NzbDrone.Core.Download.Clients.Transmission
|
||||
namespace NzbDrone.Core.Download.Clients.Transmission
|
||||
{
|
||||
public class TransmissionTorrent
|
||||
{
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,9 +57,9 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
|
|||
return hash;
|
||||
}
|
||||
|
||||
protected override string AddFromTorrentFile(RemoteAlbum remoteEpisode, string hash, string filename, byte[] fileContent)
|
||||
protected override string AddFromTorrentFile(RemoteAlbum remoteAlbum, string hash, string filename, byte[] fileContent)
|
||||
{
|
||||
var priority = (RTorrentPriority)(remoteEpisode.IsRecentAlbum() ? Settings.RecentTvPriority : Settings.OlderTvPriority);
|
||||
var priority = (RTorrentPriority)(remoteAlbum.IsRecentAlbum() ? Settings.RecentTvPriority : Settings.OlderTvPriority);
|
||||
|
||||
_proxy.AddTorrentFromFile(filename, fileContent, Settings.MusicCategory, priority, Settings.TvDirectory, Settings);
|
||||
|
||||
|
@ -69,7 +69,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
|
|||
{
|
||||
_logger.Debug("rTorrent didn't add the torrent within {0} seconds: {1}.", tries * retryDelay / 1000, filename);
|
||||
|
||||
throw new ReleaseDownloadException(remoteEpisode.Release, "Downloading torrent failed");
|
||||
throw new ReleaseDownloadException(remoteAlbum.Release, "Downloading torrent failed");
|
||||
}
|
||||
|
||||
return hash;
|
||||
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -40,11 +40,12 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
|
|||
{
|
||||
_proxy.AddTorrentFromUrl(magnetLink, Settings);
|
||||
_proxy.SetTorrentLabel(hash, Settings.MusicCategory, Settings);
|
||||
_proxy.SetTorrentSeedingConfiguration(hash, remoteAlbum.SeedConfiguration, Settings);
|
||||
|
||||
var isRecentEpisode = remoteAlbum.IsRecentAlbum();
|
||||
var isRecentAlbum = remoteAlbum.IsRecentAlbum();
|
||||
|
||||
if (isRecentEpisode && Settings.RecentTvPriority == (int)UTorrentPriority.First ||
|
||||
!isRecentEpisode && Settings.OlderTvPriority == (int)UTorrentPriority.First)
|
||||
if (isRecentAlbum && Settings.RecentTvPriority == (int)UTorrentPriority.First ||
|
||||
!isRecentAlbum && Settings.OlderTvPriority == (int)UTorrentPriority.First)
|
||||
{
|
||||
_proxy.MoveTorrentToTopInQueue(hash, Settings);
|
||||
}
|
||||
|
@ -58,11 +59,12 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
|
|||
{
|
||||
_proxy.AddTorrentFromFile(filename, fileContent, Settings);
|
||||
_proxy.SetTorrentLabel(hash, Settings.MusicCategory, Settings);
|
||||
_proxy.SetTorrentSeedingConfiguration(hash, remoteAlbum.SeedConfiguration, Settings);
|
||||
|
||||
var isRecentEpisode = remoteAlbum.IsRecentAlbum();
|
||||
var isRecentAlbum = remoteAlbum.IsRecentAlbum();
|
||||
|
||||
if (isRecentEpisode && Settings.RecentTvPriority == (int)UTorrentPriority.First ||
|
||||
!isRecentEpisode && Settings.OlderTvPriority == (int)UTorrentPriority.First)
|
||||
if (isRecentAlbum && Settings.RecentTvPriority == (int)UTorrentPriority.First ||
|
||||
!isRecentAlbum && Settings.OlderTvPriority == (int)UTorrentPriority.First)
|
||||
{
|
||||
_proxy.MoveTorrentToTopInQueue(hash, Settings);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
|
|||
{
|
||||
int GetVersion(UTorrentSettings settings);
|
||||
Dictionary<string, string> GetConfig(UTorrentSettings settings);
|
||||
UTorrentResponse GetTorrents(string cacheID, UTorrentSettings settings);
|
||||
UTorrentResponse GetTorrents(string cacheId, UTorrentSettings settings);
|
||||
|
||||
void AddTorrentFromUrl(string torrentUrl, UTorrentSettings settings);
|
||||
void AddTorrentFromFile(string fileName, byte[] fileContent, UTorrentSettings settings);
|
||||
|
@ -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,22 @@ 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);
|
||||
|
||||
|
|
|
@ -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; }
|
||||
|
|
|
@ -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;
|
||||
|
@ -25,6 +26,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,
|
||||
|
@ -32,6 +34,7 @@ namespace NzbDrone.Core.Download
|
|||
IIndexerStatusService indexerStatusService,
|
||||
IRateLimitService rateLimitService,
|
||||
IEventAggregator eventAggregator,
|
||||
ISeedConfigProvider seedConfigProvider,
|
||||
Logger logger)
|
||||
{
|
||||
_downloadClientProvider = downloadClientProvider;
|
||||
|
@ -39,6 +42,7 @@ namespace NzbDrone.Core.Download
|
|||
_indexerStatusService = indexerStatusService;
|
||||
_rateLimitService = rateLimitService;
|
||||
_eventAggregator = eventAggregator;
|
||||
_seedConfigProvider = seedConfigProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
|
@ -55,6 +59,9 @@ namespace NzbDrone.Core.Download
|
|||
throw new DownloadClientUnavailableException($"{remoteAlbum.Release.DownloadProtocol} Download client isn't configured yet");
|
||||
}
|
||||
|
||||
// Get the seed configuration for this release.
|
||||
remoteAlbum.SeedConfiguration = _seedConfigProvider.GetSeedConfiguration(remoteAlbum);
|
||||
|
||||
// Limit grabs to 2 per second.
|
||||
if (remoteAlbum.Release.DownloadUrl.IsNotNullOrWhiteSpace() && !remoteAlbum.Release.DownloadUrl.StartsWith("magnet:"))
|
||||
{
|
||||
|
|
|
@ -39,6 +39,9 @@ namespace NzbDrone.Core.Indexers.Gazelle
|
|||
[FieldDefinition(3, Type = FieldType.Textbox, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)]
|
||||
public int MinimumSeeders { get; set; }
|
||||
|
||||
[FieldDefinition(4)]
|
||||
public SeedCriteriaSettings SeedCriteria { get; } = new SeedCriteriaSettings();
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
|
|
|
@ -35,6 +35,9 @@ namespace NzbDrone.Core.Indexers.IPTorrents
|
|||
[FieldDefinition(1, Type = FieldType.Textbox, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)]
|
||||
public int MinimumSeeders { get; set; }
|
||||
|
||||
[FieldDefinition(2)]
|
||||
public SeedCriteriaSettings SeedCriteria { get; } = new SeedCriteriaSettings();
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
|
|
|
@ -3,5 +3,7 @@ namespace NzbDrone.Core.Indexers
|
|||
public interface ITorrentIndexerSettings : IIndexerSettings
|
||||
{
|
||||
int MinimumSeeders { get; set; }
|
||||
|
||||
SeedCriteriaSettings SeedCriteria { get; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,6 +33,9 @@ namespace NzbDrone.Core.Indexers.Nyaa
|
|||
[FieldDefinition(2, Type = FieldType.Textbox, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)]
|
||||
public int MinimumSeeders { get; set; }
|
||||
|
||||
[FieldDefinition(3)]
|
||||
public SeedCriteriaSettings SeedCriteria { get; } = new SeedCriteriaSettings();
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
|
|
|
@ -9,6 +9,8 @@ namespace NzbDrone.Core.Indexers.Rarbg
|
|||
public RarbgSettingsValidator()
|
||||
{
|
||||
RuleFor(c => c.BaseUrl).ValidRootUrl();
|
||||
|
||||
RuleFor(c => c.SeedCriteria).SetValidator(_ => new SeedCriteriaSettingsValidator());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -35,6 +37,9 @@ namespace NzbDrone.Core.Indexers.Rarbg
|
|||
[FieldDefinition(3, Type = FieldType.Textbox, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)]
|
||||
public int MinimumSeeders { get; set; }
|
||||
|
||||
[FieldDefinition(4)]
|
||||
public SeedCriteriaSettings SeedCriteria { get; } = new SeedCriteriaSettings();
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Download.Clients;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.Indexers
|
||||
{
|
||||
public interface ISeedConfigProvider
|
||||
{
|
||||
TorrentSeedConfiguration GetSeedConfiguration(RemoteAlbum release);
|
||||
}
|
||||
|
||||
public class SeedConfigProvider : ISeedConfigProvider
|
||||
{
|
||||
private readonly IIndexerFactory _indexerFactory;
|
||||
|
||||
public SeedConfigProvider(IIndexerFactory indexerFactory)
|
||||
{
|
||||
_indexerFactory = indexerFactory;
|
||||
}
|
||||
|
||||
public TorrentSeedConfiguration GetSeedConfiguration(RemoteAlbum remoteAlbum)
|
||||
{
|
||||
if (remoteAlbum.Release.DownloadProtocol != DownloadProtocol.Torrent)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (remoteAlbum.Release.IndexerId == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var indexer = _indexerFactory.Get(remoteAlbum.Release.IndexerId);
|
||||
var torrentIndexerSettings = indexer.Settings as ITorrentIndexerSettings;
|
||||
|
||||
if (torrentIndexerSettings != null && torrentIndexerSettings.SeedCriteria != null)
|
||||
{
|
||||
var seedConfig = new TorrentSeedConfiguration
|
||||
{
|
||||
Ratio = torrentIndexerSettings.SeedCriteria.SeedRatio
|
||||
};
|
||||
|
||||
var seedTime = remoteAlbum.ParsedAlbumInfo.Discography ? torrentIndexerSettings.SeedCriteria.DiscographySeedTime : torrentIndexerSettings.SeedCriteria.SeedTime;
|
||||
if (seedTime.HasValue)
|
||||
{
|
||||
seedConfig.SeedTime = TimeSpan.FromMinutes(seedTime.Value);
|
||||
}
|
||||
|
||||
return seedConfig;
|
||||
}
|
||||
}
|
||||
catch (ModelNotFoundException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using FluentValidation;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers
|
||||
{
|
||||
public class SeedCriteriaSettingsValidator : AbstractValidator<SeedCriteriaSettings>
|
||||
{
|
||||
public SeedCriteriaSettingsValidator(double seedRatioMinimum = 0.0, int seedTimeMinimum = 0, int discographySeedTimeMinimum = 0)
|
||||
{
|
||||
RuleFor(c => c.SeedRatio).GreaterThan(0.0)
|
||||
.When(c => c.SeedRatio.HasValue)
|
||||
.AsWarning().WithMessage("Should be greater than zero");
|
||||
|
||||
RuleFor(c => c.SeedTime).GreaterThan(0)
|
||||
.When(c => c.SeedTime.HasValue)
|
||||
.AsWarning().WithMessage("Should be greater than zero");
|
||||
|
||||
RuleFor(c => c.DiscographySeedTime).GreaterThan(0)
|
||||
.When(c => c.DiscographySeedTime.HasValue)
|
||||
.AsWarning().WithMessage("Should be greater than zero");
|
||||
|
||||
if (seedRatioMinimum != 0.0)
|
||||
{
|
||||
RuleFor(c => c.SeedRatio).GreaterThanOrEqualTo(seedRatioMinimum)
|
||||
.When(c => c.SeedRatio > 0.0)
|
||||
.AsWarning()
|
||||
.WithMessage($"Under {seedRatioMinimum} leads to H&R");
|
||||
}
|
||||
|
||||
if (seedTimeMinimum != 0)
|
||||
{
|
||||
RuleFor(c => c.SeedTime).GreaterThanOrEqualTo(seedTimeMinimum)
|
||||
.When(c => c.SeedTime > 0)
|
||||
.AsWarning()
|
||||
.WithMessage($"Under {seedTimeMinimum} leads to H&R");
|
||||
}
|
||||
|
||||
if (discographySeedTimeMinimum != 0)
|
||||
{
|
||||
RuleFor(c => c.DiscographySeedTime).GreaterThanOrEqualTo(discographySeedTimeMinimum)
|
||||
.When(c => c.DiscographySeedTime > 0)
|
||||
.AsWarning()
|
||||
.WithMessage($"Under {discographySeedTimeMinimum} leads to H&R");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class SeedCriteriaSettings
|
||||
{
|
||||
private static readonly SeedCriteriaSettingsValidator Validator = new SeedCriteriaSettingsValidator();
|
||||
|
||||
[FieldDefinition(0, Type = FieldType.Textbox, Label = "Seed Ratio", HelpText = "The ratio a torrent should reach before stopping, empty is download client's default", Advanced = true)]
|
||||
public double? SeedRatio { get; set; }
|
||||
|
||||
[FieldDefinition(1, Type = FieldType.Textbox, Label = "Seed Time", Unit = "minutes", HelpText = "The time a torrent should be seeded before stopping, empty is download client's default", Advanced = true)]
|
||||
public int? SeedTime { get; set; }
|
||||
|
||||
[FieldDefinition(2, Type = FieldType.Textbox, Label = "Discography Seed Time", Unit = "minutes", HelpText = "The time a torrent should be seeded before stopping, empty is download client's default", Advanced = true)]
|
||||
public int? DiscographySeedTime { get; set; }
|
||||
}
|
||||
}
|
|
@ -9,6 +9,8 @@ namespace NzbDrone.Core.Indexers.TorrentRss
|
|||
public TorrentRssIndexerSettingsValidator()
|
||||
{
|
||||
RuleFor(c => c.BaseUrl).ValidRootUrl();
|
||||
|
||||
RuleFor(c => c.SeedCriteria).SetValidator(_ => new SeedCriteriaSettingsValidator());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -35,6 +37,9 @@ namespace NzbDrone.Core.Indexers.TorrentRss
|
|||
[FieldDefinition(3, Type = FieldType.Textbox, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)]
|
||||
public int MinimumSeeders { get; set; }
|
||||
|
||||
[FieldDefinition(4)]
|
||||
public SeedCriteriaSettings SeedCriteria { get; } = new SeedCriteriaSettings();
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(validator.Validate(this));
|
||||
|
|
|
@ -10,6 +10,8 @@ namespace NzbDrone.Core.Indexers.Torrentleech
|
|||
{
|
||||
RuleFor(c => c.BaseUrl).ValidRootUrl();
|
||||
RuleFor(c => c.ApiKey).NotEmpty();
|
||||
|
||||
RuleFor(c => c.SeedCriteria).SetValidator(_ => new SeedCriteriaSettingsValidator());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -32,6 +34,9 @@ namespace NzbDrone.Core.Indexers.Torrentleech
|
|||
[FieldDefinition(2, Type = FieldType.Textbox, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)]
|
||||
public int MinimumSeeders { get; set; }
|
||||
|
||||
[FieldDefinition(3)]
|
||||
public SeedCriteriaSettings SeedCriteria { get; } = new SeedCriteriaSettings();
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
|
|
|
@ -45,6 +45,8 @@ namespace NzbDrone.Core.Indexers.Torznab
|
|||
RuleFor(c => c.ApiKey).NotEmpty().When(ShouldHaveApiKey);
|
||||
RuleFor(c => c.AdditionalParameters).Matches(AdditionalParametersRegex)
|
||||
.When(c => !c.AdditionalParameters.IsNullOrWhiteSpace());
|
||||
|
||||
RuleFor(c => c.SeedCriteria).SetValidator(_ => new SeedCriteriaSettingsValidator());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -60,6 +62,9 @@ namespace NzbDrone.Core.Indexers.Torznab
|
|||
[FieldDefinition(5, Type = FieldType.Textbox, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)]
|
||||
public int MinimumSeeders { get; set; }
|
||||
|
||||
[FieldDefinition(6)]
|
||||
public SeedCriteriaSettings SeedCriteria { get; } = new SeedCriteriaSettings();
|
||||
|
||||
public override NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
|
|
|
@ -37,6 +37,9 @@ namespace NzbDrone.Core.Indexers.Waffles
|
|||
[FieldDefinition(3, Type = FieldType.Textbox, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)]
|
||||
public int MinimumSeeders { get; set; }
|
||||
|
||||
[FieldDefinition(4)]
|
||||
public SeedCriteriaSettings SeedCriteria { get; } = new SeedCriteriaSettings();
|
||||
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
|
|
|
@ -581,6 +581,8 @@
|
|||
<Compile Include="Indexers\IndexerDefaults.cs" />
|
||||
<Compile Include="Indexers\ITorrentIndexerSettings.cs" />
|
||||
<Compile Include="Indexers\RssEnclosure.cs" />
|
||||
<Compile Include="Indexers\SeedConfigProvider.cs" />
|
||||
<Compile Include="Indexers\SeedCriteriaSettings.cs" />
|
||||
<Compile Include="Indexers\Waffles\WafflesRssParser.cs" />
|
||||
<Compile Include="Indexers\Waffles\Waffles.cs" />
|
||||
<Compile Include="Indexers\Waffles\WafflesRequestGenerator.cs" />
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.Download.Clients;
|
||||
using NzbDrone.Core.Music;
|
||||
|
||||
namespace NzbDrone.Core.Parser.Model
|
||||
|
@ -12,6 +13,7 @@ namespace NzbDrone.Core.Parser.Model
|
|||
public Artist Artist { get; set; }
|
||||
public List<Album> Albums { get; set; }
|
||||
public bool DownloadAllowed { get; set; }
|
||||
public TorrentSeedConfiguration SeedConfiguration { get; set; }
|
||||
|
||||
public bool IsRecentAlbum()
|
||||
{
|
||||
|
@ -23,4 +25,4 @@ namespace NzbDrone.Core.Parser.Model
|
|||
return Release.Title;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue