New: Nested Settings and Seed Ratio Setting (#379)

* New: Nested Settings and Seed Ratio Setting

* Fixed: Sonarr related variable naming
pull/387/head
Qstick 6 years ago committed by GitHub
parent dabb9bc18a
commit 089d213816
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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,25 +12,88 @@ 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);
var field = mapping.Field.Clone();
field.Value = mapping.GetterFunc(model);
result.Add(field);
}
return result.OrderBy(r => r.Order).ToList();
}
public static object ReadFromSchema(List<Field> fields, Type targetType)
{
Ensure.That(targetType, () => targetType).IsNotNull();
var mappings = GetFieldMappings(targetType);
var target = Activator.CreateInstance(targetType);
foreach (var mapping in mappings)
{
var field = fields.Find(f => f.Name == mapping.Field.Name);
mapping.SetterFunc(target, field.Value);
}
return target;
}
public static T ReadFromSchema<T>(List<Field> fields)
{
return (T) ReadFromSchema(fields, typeof(T));
}
if (fieldAttribute != null)
// 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 = propertyInfo.Name,
Name = prefix + propertyInfo.Name,
Label = fieldAttribute.Label,
Unit = fieldAttribute.Unit,
HelpText = fieldAttribute.HelpText,
HelpLink = fieldAttribute.HelpLink,
Order = fieldAttribute.Order,
@ -37,120 +101,113 @@ namespace Lidarr.Http.ClientSchema
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);
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.OrderBy(r => r.Order).ToList();
return result.ToArray();
}
public static object ReadFromSchema(List<Field> fields, Type targetType)
private static Tuple<PropertyInfo, FieldDefinitionAttribute>[] GetProperties(Type type)
{
Ensure.That(targetType, () => targetType).IsNotNull();
var properties = targetType.GetSimpleProperties();
return type.GetProperties()
.Select(v => Tuple.Create(v, v.GetAttribute<FieldDefinitionAttribute>(false)))
.Where(v => v.Item2 != null)
.OrderBy(v => v.Item2.Order)
.ToArray();
}
var target = Activator.CreateInstance(targetType);
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;
}
foreach (var propertyInfo in properties)
else if (propertyType == typeof(long))
{
var fieldAttribute = propertyInfo.GetAttribute<FieldDefinitionAttribute>(false);
return fieldValue => fieldValue?.ToString().ParseInt64() ?? 0;
}
if (fieldAttribute != null)
{
var field = fields.Find(f => f.Name == propertyInfo.Name);
else if (propertyType == typeof(double))
{
return fieldValue => fieldValue?.ToString().ParseDouble() ?? 0.0;
}
if (propertyInfo.PropertyType == typeof(int))
{
var value = field.Value.ToString().ParseInt32();
propertyInfo.SetValue(target, value ?? 0, null);
}
else if (propertyType == typeof(int?))
{
return fieldValue => fieldValue?.ToString().ParseInt32();
}
else if (propertyInfo.PropertyType == typeof(long))
{
var value = field.Value.ToString().ParseInt64();
propertyInfo.SetValue(target, value ?? 0, null);
}
else if (propertyType == typeof(Int64?))
{
return fieldValue => fieldValue?.ToString().ParseInt64();
}
else if (propertyInfo.PropertyType == typeof(int?))
{
var value = field.Value.ToString().ParseInt32();
propertyInfo.SetValue(target, value, null);
}
else if (propertyType == typeof(double?))
{
return fieldValue => fieldValue?.ToString().ParseDouble();
}
else if (propertyInfo.PropertyType == typeof(Nullable<Int64>))
else if (propertyType == typeof(IEnumerable<int>))
{
return fieldValue =>
{
if (fieldValue.GetType() == typeof(JArray))
{
var value = field.Value.ToString().ParseInt64();
propertyInfo.SetValue(target, value, null);
return ((JArray) fieldValue).Select(s => s.Value<int>());
}
else if (propertyInfo.PropertyType == typeof(IEnumerable<int>))
else
{
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);
return fieldValue.ToString().Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries)
.Select(s => Convert.ToInt32(s));
}
};
}
else if (propertyInfo.PropertyType == typeof(IEnumerable<string>))
else if (propertyType == typeof(IEnumerable<string>))
{
return fieldValue =>
{
if (fieldValue.GetType() == typeof(JArray))
{
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);
return ((JArray) fieldValue).Select(s => s.Value<string>());
}
else
{
propertyInfo.SetValue(target, field.Value, null);
return fieldValue.ToString().Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries);
}
}
};
}
return target;
}
public static T ReadFromSchema<T>(List<Field> fields)
{
return (T)ReadFromSchema(fields, typeof(T));
}
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();
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…
Cancel
Save