diff --git a/src/Lidarr.Api.V1/Indexers/ReleasePushModule.cs b/src/Lidarr.Api.V1/Indexers/ReleasePushModule.cs index 344ee777f..9ecf45a78 100644 --- a/src/Lidarr.Api.V1/Indexers/ReleasePushModule.cs +++ b/src/Lidarr.Api.V1/Indexers/ReleasePushModule.cs @@ -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()); @@ -41,10 +47,47 @@ namespace Lidarr.Api.V1.Indexers info.Guid = "PUSH-" + info.DownloadUrl; + ResolveIndexer(info); + var decisions = _downloadDecisionMaker.GetRssDecision(new List { 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); + } + } } } diff --git a/src/Lidarr.Api.V1/ProviderModuleBase.cs b/src/Lidarr.Api.V1/ProviderModuleBase.cs index 24d965898..848168637 100644 --- a/src/Lidarr.Api.V1/ProviderModuleBase.cs +++ b/src/Lidarr.Api.V1/ProviderModuleBase.cs @@ -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)) { diff --git a/src/Lidarr.Http/ClientSchema/Field.cs b/src/Lidarr.Http/ClientSchema/Field.cs index 332ed871d..7b27d4c4d 100644 --- a/src/Lidarr.Http/ClientSchema/Field.cs +++ b/src/Lidarr.Http/ClientSchema/Field.cs @@ -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 SelectOptions { get; set; } + + public Field Clone() + { + return (Field) MemberwiseClone(); + } } -} \ No newline at end of file +} diff --git a/src/Lidarr.Http/ClientSchema/FieldMapping.cs b/src/Lidarr.Http/ClientSchema/FieldMapping.cs new file mode 100644 index 000000000..e8ce91824 --- /dev/null +++ b/src/Lidarr.Http/ClientSchema/FieldMapping.cs @@ -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 GetterFunc { get; set; } + public Action SetterFunc { get; set; } + } +} diff --git a/src/Lidarr.Http/ClientSchema/SchemaBuilder.cs b/src/Lidarr.Http/ClientSchema/SchemaBuilder.cs index c6b8240d5..880ce1726 100644 --- a/src/Lidarr.Http/ClientSchema/SchemaBuilder.cs +++ b/src/Lidarr.Http/ClientSchema/SchemaBuilder.cs @@ -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 _mappings = new Dictionary(); + public static List ToSchema(object model) { Ensure.That(model, () => model).IsNotNull(); - var properties = model.GetType().GetSimpleProperties(); + var mappings = GetFieldMappings(model.GetType()); - var result = new List(properties.Count); + var result = new List(mappings.Length); - foreach (var propertyInfo in properties) + foreach (var mapping in mappings) { - var fieldAttribute = propertyInfo.GetAttribute(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 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(List 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 targetSelector) + { + var result = new List(); + 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 fields, Type targetType) + private static Tuple[] GetProperties(Type type) { - Ensure.That(targetType, () => targetType).IsNotNull(); - - var properties = targetType.GetSimpleProperties(); + return type.GetProperties() + .Select(v => Tuple.Create(v, v.GetAttribute(false))) + .Where(v => v.Item2 != null) + .OrderBy(v => v.Item2.Order) + .ToArray(); + } - var target = Activator.CreateInstance(targetType); + private static List 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 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(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)) + else if (propertyType == typeof(IEnumerable)) + { + 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()); } - else if (propertyInfo.PropertyType == typeof(IEnumerable)) + else { - IEnumerable value; - - if (field.Value.GetType() == typeof(JArray)) - { - value = ((JArray)field.Value).Select(s => s.Value()); - } - - 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)) + else if (propertyType == typeof(IEnumerable)) + { + return fieldValue => + { + if (fieldValue.GetType() == typeof(JArray)) { - IEnumerable value; - - if (field.Value.GetType() == typeof(JArray)) - { - value = ((JArray)field.Value).Select(s => s.Value()); - } - - else - { - value = field.Value.ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - } - - propertyInfo.SetValue(target, value, null); + return ((JArray) fieldValue).Select(s => s.Value()); } - else { - propertyInfo.SetValue(target, field.Value, null); + return fieldValue.ToString().Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries); } - } + }; } - return target; - - } - - public static T ReadFromSchema(List fields) - { - return (T)ReadFromSchema(fields, typeof(T)); - } - - private static List 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; + } } } } diff --git a/src/Lidarr.Http/Lidarr.Http.csproj b/src/Lidarr.Http/Lidarr.Http.csproj index 8fa4797ba..237f3bbfd 100644 --- a/src/Lidarr.Http/Lidarr.Http.csproj +++ b/src/Lidarr.Http/Lidarr.Http.csproj @@ -65,6 +65,7 @@ + diff --git a/src/NzbDrone.Api.Test/ClientSchemaTests/SchemaBuilderFixture.cs b/src/NzbDrone.Api.Test/ClientSchemaTests/SchemaBuilderFixture.cs index e78709c2f..f8fbbb002 100644 --- a/src/NzbDrone.Api.Test/ClientSchemaTests/SchemaBuilderFixture.cs +++ b/src/NzbDrone.Api.Test/ClientSchemaTests/SchemaBuilderFixture.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; } + } } diff --git a/src/NzbDrone.Common/Extensions/TryParseExtensions.cs b/src/NzbDrone.Common/Extensions/TryParseExtensions.cs index c485fbd54..21255f514 100644 --- a/src/NzbDrone.Common/Extensions/TryParseExtensions.cs +++ b/src/NzbDrone.Common/Extensions/TryParseExtensions.cs @@ -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 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; + } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadStationTests/TorrentDownloadStationFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadStationTests/TorrentDownloadStationFixture.cs index 5b0821a5e..b68ff1388 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadStationTests/TorrentDownloadStationFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadStationTests/TorrentDownloadStationFixture.cs @@ -73,6 +73,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests Transfer = new Dictionary { { "size_downloaded", "0"}, + { "size_uploaded", "0"}, { "speed_download", "0" } } } @@ -96,6 +97,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests Transfer = new Dictionary { { "size_downloaded", "1000"}, + { "size_uploaded", "100"}, { "speed_download", "0" } }, } @@ -119,6 +121,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests Transfer = new Dictionary { { "size_downloaded", "1000"}, + { "size_uploaded", "100"}, { "speed_download", "0" } } } @@ -142,6 +145,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests Transfer = new Dictionary { { "size_downloaded", "100"}, + { "size_uploaded", "10"}, { "speed_download", "50" } } } @@ -165,6 +169,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests Transfer = new Dictionary { { "size_downloaded", "10"}, + { "size_uploaded", "1"}, { "speed_download", "0" } } } @@ -188,6 +193,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests Transfer = new Dictionary { { "size_downloaded", "1000"}, + { "size_uploaded", "100"}, { "speed_download", "0" } } } @@ -211,6 +217,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests Transfer = new Dictionary { { "size_downloaded", "1000"}, + { "size_uploaded", "100"}, { "speed_download", "0" } } } @@ -234,6 +241,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests Transfer = new Dictionary { { "size_downloaded", "1000"}, + { "size_uploaded", "100"}, { "speed_download", "0" } } } @@ -257,6 +265,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests Transfer = new Dictionary { { "size_downloaded", "1000"}, + { "size_uploaded", "100"}, { "speed_download", "0" } } } diff --git a/src/NzbDrone.Core.Test/IndexerTests/SeedConfigProviderFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/SeedConfigProviderFixture.cs new file mode 100644 index 000000000..53a054868 --- /dev/null +++ b/src/NzbDrone.Core.Test/IndexerTests/SeedConfigProviderFixture.cs @@ -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 + { + [Test] + public void should_not_return_config_for_non_existent_indexer() + { + Mocker.GetMock() + .Setup(v => v.Get(It.IsAny())) + .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() + .Setup(v => v.Get(It.IsAny())) + .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)); + } + } +} diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index c0a5ab2b2..64349108c 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -253,6 +253,7 @@ + diff --git a/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs b/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs index 32e77e266..679fb4a84 100644 --- a/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs +++ b/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.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; } diff --git a/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs b/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs index 16ca7926e..4910c64ab 100644 --- a/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs +++ b/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs @@ -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); } diff --git a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeProxy.cs b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeProxy.cs index ac1823340..d45ab725e 100644 --- a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeProxy.cs @@ -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(); + if (seedConfiguration.Ratio != null) { - var ratioArguments = new Dictionary(); ratioArguments.Add("stop_ratio", seedConfiguration.Ratio.Value); - - ProcessRequest(settings, "core.set_torrent_options", new string[] { hash }, ratioArguments); + ratioArguments.Add("stop_at_ratio", 1); } + + ProcessRequest(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); diff --git a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeTorrent.cs b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeTorrent.cs index 5dcdc7549..d4b46ea42 100644 --- a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeTorrent.cs @@ -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")] diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs index e0103cf71..70f83d6b7 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs @@ -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 diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/UsenetDownloadStation.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/UsenetDownloadStation.cs index 62b018351..94420c57c 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/UsenetDownloadStation.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/UsenetDownloadStation.cs @@ -147,7 +147,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation { _logger.Debug(e, "Failed to get config from Download Station"); - throw e; + throw; } } diff --git a/src/NzbDrone.Core/Download/Clients/Hadouken/Hadouken.cs b/src/NzbDrone.Core/Download/Clients/Hadouken/Hadouken.cs index 43836b7e3..d4bd188c9 100644 --- a/src/NzbDrone.Core/Download/Clients/Hadouken/Hadouken.cs +++ b/src/NzbDrone.Core/Download/Clients/Hadouken/Hadouken.cs @@ -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)) diff --git a/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenProxy.cs b/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenProxy.cs index 8a99728d4..5a2a53dd1 100644 --- a/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenProxy.cs @@ -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]), diff --git a/src/NzbDrone.Core/Download/Clients/Hadouken/Models/HadoukenTorrent.cs b/src/NzbDrone.Core/Download/Clients/Hadouken/Models/HadoukenTorrent.cs index a52180ca2..898a09f69 100644 --- a/src/NzbDrone.Core/Download/Clients/Hadouken/Models/HadoukenTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/Hadouken/Models/HadoukenTorrent.cs @@ -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; } } diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs index 757a2f1b2..160a2514f 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs @@ -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)); diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs index b05f073dc..ba30ccf64 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs @@ -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() diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs index a365046fa..43dc2711c 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs @@ -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(); - 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(); diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionTorrent.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionTorrent.cs index 3845ce0b0..10fee3a50 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionTorrent.cs @@ -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; } } } diff --git a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs index cf0b064bc..ec2399026 100644 --- a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs @@ -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) { diff --git a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs index 39fddc8b0..ea7fe3db6 100644 --- a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs @@ -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); } diff --git a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentProxy.cs b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentProxy.cs index c594ebb58..8f8940e76 100644 --- a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentProxy.cs @@ -13,7 +13,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent { int GetVersion(UTorrentSettings settings); Dictionary 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); diff --git a/src/NzbDrone.Core/Download/DownloadClientItem.cs b/src/NzbDrone.Core/Download/DownloadClientItem.cs index eab9e431e..3348be4a9 100644 --- a/src/NzbDrone.Core/Download/DownloadClientItem.cs +++ b/src/NzbDrone.Core/Download/DownloadClientItem.cs @@ -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; } diff --git a/src/NzbDrone.Core/Download/DownloadService.cs b/src/NzbDrone.Core/Download/DownloadService.cs index 8ff6cf2ce..7f3b20c2a 100644 --- a/src/NzbDrone.Core/Download/DownloadService.cs +++ b/src/NzbDrone.Core/Download/DownloadService.cs @@ -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:")) { diff --git a/src/NzbDrone.Core/Indexers/Gazelle/GazelleSettings.cs b/src/NzbDrone.Core/Indexers/Gazelle/GazelleSettings.cs index 428a99b9e..eccd73cdd 100644 --- a/src/NzbDrone.Core/Indexers/Gazelle/GazelleSettings.cs +++ b/src/NzbDrone.Core/Indexers/Gazelle/GazelleSettings.cs @@ -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)); diff --git a/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsSettings.cs b/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsSettings.cs index a99b9940b..9ea20e1a7 100644 --- a/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsSettings.cs +++ b/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsSettings.cs @@ -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)); diff --git a/src/NzbDrone.Core/Indexers/ITorrentIndexerSettings.cs b/src/NzbDrone.Core/Indexers/ITorrentIndexerSettings.cs index 150f63424..10b885429 100644 --- a/src/NzbDrone.Core/Indexers/ITorrentIndexerSettings.cs +++ b/src/NzbDrone.Core/Indexers/ITorrentIndexerSettings.cs @@ -3,5 +3,7 @@ namespace NzbDrone.Core.Indexers public interface ITorrentIndexerSettings : IIndexerSettings { int MinimumSeeders { get; set; } + + SeedCriteriaSettings SeedCriteria { get; } } } diff --git a/src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs b/src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs index 6b997021c..1d07141bc 100644 --- a/src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs +++ b/src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs @@ -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)); diff --git a/src/NzbDrone.Core/Indexers/Rarbg/RarbgSettings.cs b/src/NzbDrone.Core/Indexers/Rarbg/RarbgSettings.cs index 33ca45404..af99f9c19 100644 --- a/src/NzbDrone.Core/Indexers/Rarbg/RarbgSettings.cs +++ b/src/NzbDrone.Core/Indexers/Rarbg/RarbgSettings.cs @@ -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)); diff --git a/src/NzbDrone.Core/Indexers/SeedConfigProvider.cs b/src/NzbDrone.Core/Indexers/SeedConfigProvider.cs new file mode 100644 index 000000000..f1e250ce9 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/SeedConfigProvider.cs @@ -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; + } + } +} diff --git a/src/NzbDrone.Core/Indexers/SeedCriteriaSettings.cs b/src/NzbDrone.Core/Indexers/SeedCriteriaSettings.cs new file mode 100644 index 000000000..08b9f00b5 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/SeedCriteriaSettings.cs @@ -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 + { + 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; } + } +} diff --git a/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerSettings.cs b/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerSettings.cs index 7edcc1680..378a30b46 100644 --- a/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerSettings.cs +++ b/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerSettings.cs @@ -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)); diff --git a/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechSettings.cs b/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechSettings.cs index a76690478..b8bcf6414 100644 --- a/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechSettings.cs +++ b/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechSettings.cs @@ -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)); diff --git a/src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs b/src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs index 0c2cda826..19b353c99 100644 --- a/src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs +++ b/src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs @@ -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)); diff --git a/src/NzbDrone.Core/Indexers/Waffles/WafflesSettings.cs b/src/NzbDrone.Core/Indexers/Waffles/WafflesSettings.cs index 70ef5eba4..add45c1c5 100644 --- a/src/NzbDrone.Core/Indexers/Waffles/WafflesSettings.cs +++ b/src/NzbDrone.Core/Indexers/Waffles/WafflesSettings.cs @@ -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() { diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index c44c8ffb1..e1f6dc4c7 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -581,6 +581,8 @@ + + diff --git a/src/NzbDrone.Core/Parser/Model/RemoteAlbum.cs b/src/NzbDrone.Core/Parser/Model/RemoteAlbum.cs index 9d303bbae..1c15d64c4 100644 --- a/src/NzbDrone.Core/Parser/Model/RemoteAlbum.cs +++ b/src/NzbDrone.Core/Parser/Model/RemoteAlbum.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 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; } } -} \ No newline at end of file +}