From 8a6d1ef373dc0e3a9626d91a58e84d2ff2cb717c Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 8 Apr 2016 13:23:09 -0700 Subject: [PATCH] Release scoring --- .../PrioritizeDownloadDecisionFixture.cs | 153 +++++++++++++++++- .../DownloadDecisionComparer.cs | 151 +++++++++++++++++ .../DownloadDecisionPriorizationService.cs | 33 +--- src/NzbDrone.Core/NzbDrone.Core.csproj | 1 + src/NzbDrone.Core/Parser/Model/TorrentInfo.cs | 13 ++ 5 files changed, 319 insertions(+), 32 deletions(-) create mode 100644 src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/PrioritizeDownloadDecisionFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/PrioritizeDownloadDecisionFixture.cs index 47e3ed127..148169d9c 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/PrioritizeDownloadDecisionFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/PrioritizeDownloadDecisionFixture.cs @@ -12,6 +12,7 @@ using NzbDrone.Core.DecisionEngine; using NUnit.Framework; using FluentAssertions; using FizzWare.NBuilder; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Test.Framework; namespace NzbDrone.Core.Test.DecisionEngineTests @@ -121,7 +122,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests } [Test] - public void should_order_by_smallest_rounded_to_200mb_then_age() + public void should_order_by_age_then_largest_rounded_to_200mb() { var remoteEpisodeSd = GivenRemoteEpisode(new List { GivenEpisode(1) }, new QualityModel(Quality.SDTV), size: 100.Megabytes(), age: 1); var remoteEpisodeHdSmallOld = GivenRemoteEpisode(new List { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p), size: 1200.Megabytes(), age: 1000); @@ -135,7 +136,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests decisions.Add(new DownloadDecision(remoteEpisodeHdLargeYoung)); var qualifiedReports = Subject.PrioritizeDecisions(decisions); - qualifiedReports.First().RemoteEpisode.Should().Be(remoteEpisodeSmallYoung); + qualifiedReports.First().RemoteEpisode.Should().Be(remoteEpisodeHdLargeYoung); } [Test] @@ -199,5 +200,153 @@ namespace NzbDrone.Core.Test.DecisionEngineTests var qualifiedReports = Subject.PrioritizeDecisions(decisions); qualifiedReports.First().RemoteEpisode.Release.DownloadProtocol.Should().Be(DownloadProtocol.Torrent); } + + [Test] + public void should_prefer_season_pack_above_single_episode() + { + var remoteEpisode1 = GivenRemoteEpisode(new List { GivenEpisode(1), GivenEpisode(2) }, new QualityModel(Quality.HDTV720p)); + var remoteEpisode2 = GivenRemoteEpisode(new List { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p)); + + remoteEpisode1.ParsedEpisodeInfo.FullSeason = true; + + var decisions = new List(); + decisions.Add(new DownloadDecision(remoteEpisode1)); + decisions.Add(new DownloadDecision(remoteEpisode2)); + + var qualifiedReports = Subject.PrioritizeDecisions(decisions); + qualifiedReports.First().RemoteEpisode.ParsedEpisodeInfo.FullSeason.Should().BeTrue(); + } + + [Test] + public void should_prefer_releases_with_more_seeders() + { + var remoteEpisode1 = GivenRemoteEpisode(new List { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p)); + var remoteEpisode2 = GivenRemoteEpisode(new List { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p)); + + var torrentInfo1 = new TorrentInfo(); + torrentInfo1.PublishDate = DateTime.Now; + torrentInfo1.Size = 0; + torrentInfo1.DownloadProtocol = DownloadProtocol.Torrent; + torrentInfo1.Seeders = 10; + + var torrentInfo2 = torrentInfo1.JsonClone(); + torrentInfo2.Seeders = 100; + + remoteEpisode1.Release = torrentInfo1; + remoteEpisode2.Release = torrentInfo2; + + var decisions = new List(); + decisions.Add(new DownloadDecision(remoteEpisode1)); + decisions.Add(new DownloadDecision(remoteEpisode2)); + + var qualifiedReports = Subject.PrioritizeDecisions(decisions); + ((TorrentInfo) qualifiedReports.First().RemoteEpisode.Release).Seeders.Should().Be(torrentInfo2.Seeders); + } + + [Test] + public void should_prefer_releases_with_more_peers_given_equal_number_of_seeds() + { + var remoteEpisode1 = GivenRemoteEpisode(new List { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p)); + var remoteEpisode2 = GivenRemoteEpisode(new List { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p)); + + var torrentInfo1 = new TorrentInfo(); + torrentInfo1.PublishDate = DateTime.Now; + torrentInfo1.Size = 0; + torrentInfo1.DownloadProtocol = DownloadProtocol.Torrent; + torrentInfo1.Seeders = 10; + torrentInfo1.Peers = 10; + + + var torrentInfo2 = torrentInfo1.JsonClone(); + torrentInfo2.Peers = 100; + + remoteEpisode1.Release = torrentInfo1; + remoteEpisode2.Release = torrentInfo2; + + var decisions = new List(); + decisions.Add(new DownloadDecision(remoteEpisode1)); + decisions.Add(new DownloadDecision(remoteEpisode2)); + + var qualifiedReports = Subject.PrioritizeDecisions(decisions); + ((TorrentInfo)qualifiedReports.First().RemoteEpisode.Release).Peers.Should().Be(torrentInfo2.Peers); + } + + [Test] + public void should_prefer_releases_with_more_peers_no_seeds() + { + var remoteEpisode1 = GivenRemoteEpisode(new List { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p)); + var remoteEpisode2 = GivenRemoteEpisode(new List { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p)); + + var torrentInfo1 = new TorrentInfo(); + torrentInfo1.PublishDate = DateTime.Now; + torrentInfo1.Size = 0; + torrentInfo1.DownloadProtocol = DownloadProtocol.Torrent; + torrentInfo1.Seeders = 0; + torrentInfo1.Peers = 10; + + + var torrentInfo2 = torrentInfo1.JsonClone(); + torrentInfo2.Seeders = 0; + torrentInfo2.Peers = 100; + + remoteEpisode1.Release = torrentInfo1; + remoteEpisode2.Release = torrentInfo2; + + var decisions = new List(); + decisions.Add(new DownloadDecision(remoteEpisode1)); + decisions.Add(new DownloadDecision(remoteEpisode2)); + + var qualifiedReports = Subject.PrioritizeDecisions(decisions); + ((TorrentInfo)qualifiedReports.First().RemoteEpisode.Release).Peers.Should().Be(torrentInfo2.Peers); + } + + [Test] + public void should_prefer_first_release_if_peers_and_size_are_too_similar() + { + var remoteEpisode1 = GivenRemoteEpisode(new List { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p)); + var remoteEpisode2 = GivenRemoteEpisode(new List { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p)); + + var torrentInfo1 = new TorrentInfo(); + torrentInfo1.PublishDate = DateTime.Now; + torrentInfo1.DownloadProtocol = DownloadProtocol.Torrent; + torrentInfo1.Seeders = 1000; + torrentInfo1.Peers = 10; + torrentInfo1.Size = 200.Megabytes(); + + var torrentInfo2 = torrentInfo1.JsonClone(); + torrentInfo2.Seeders = 1100; + torrentInfo2.Peers = 10; + torrentInfo1.Size = 250.Megabytes(); + + remoteEpisode1.Release = torrentInfo1; + remoteEpisode2.Release = torrentInfo2; + + var decisions = new List(); + decisions.Add(new DownloadDecision(remoteEpisode1)); + decisions.Add(new DownloadDecision(remoteEpisode2)); + + var qualifiedReports = Subject.PrioritizeDecisions(decisions); + ((TorrentInfo) qualifiedReports.First().RemoteEpisode.Release).Should().Be(torrentInfo1); + } + + [Test] + public void should_prefer_first_release_if_age_and_size_are_too_similar() + { + var remoteEpisode1 = GivenRemoteEpisode(new List { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p)); + var remoteEpisode2 = GivenRemoteEpisode(new List { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p)); + + remoteEpisode1.Release.PublishDate = DateTime.UtcNow.AddDays(-100); + remoteEpisode1.Release.Size = 200.Megabytes(); + + remoteEpisode2.Release.PublishDate = DateTime.UtcNow.AddDays(-150); + remoteEpisode2.Release.Size = 250.Megabytes(); + + var decisions = new List(); + decisions.Add(new DownloadDecision(remoteEpisode1)); + decisions.Add(new DownloadDecision(remoteEpisode2)); + + var qualifiedReports = Subject.PrioritizeDecisions(decisions); + qualifiedReports.First().RemoteEpisode.Release.Should().Be(remoteEpisode1.Release); + } } } diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs new file mode 100644 index 000000000..882105a9d --- /dev/null +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Profiles.Delay; + +namespace NzbDrone.Core.DecisionEngine +{ + public class DownloadDecisionComparer : IComparer + { + private readonly IDelayProfileService _delayProfileService; + public delegate int CompareDelegate(DownloadDecision x, DownloadDecision y); + public delegate int CompareDelegate(DownloadDecision x, DownloadDecision y); + + public DownloadDecisionComparer(IDelayProfileService delayProfileService) + { + _delayProfileService = delayProfileService; + } + + public int Compare(DownloadDecision x, DownloadDecision y) + { + var comparers = new List + { + CompareQuality, + CompareProtocol, + CompareEpisodeCount, + CompareEpisodeNumber, + ComparePeersIfTorrent, + CompareAgeIfUsenet, + CompareSize + }; + + return comparers.Select(comparer => comparer(x, y)).FirstOrDefault(result => result != 0); + } + + private int CompareBy(TSubject left, TSubject right, Func funcValue) + where TValue : IComparable + { + var leftValue = funcValue(left); + var rightValue = funcValue(right); + + return leftValue.CompareTo(rightValue); + } + + private int CompareByReverse(TSubject left, TSubject right, Func funcValue) + where TValue : IComparable + { + return CompareBy(left, right, funcValue)*-1; + } + + private int CompareAll(params int[] comparers) + { + return comparers.Select(comparer => comparer).FirstOrDefault(result => result != 0); + } + + private int CompareQuality(DownloadDecision x, DownloadDecision y) + { + return CompareAll(CompareBy(x.RemoteEpisode, y.RemoteEpisode, remoteEpisode => remoteEpisode.Series.Profile.Value.Items.FindIndex(v => v.Quality == remoteEpisode.ParsedEpisodeInfo.Quality.Quality)), + CompareBy(x.RemoteEpisode, y.RemoteEpisode, remoteEpisode => remoteEpisode.ParsedEpisodeInfo.Quality.Revision.Real), + CompareBy(x.RemoteEpisode, y.RemoteEpisode, remoteEpisode => remoteEpisode.ParsedEpisodeInfo.Quality.Revision.Version)); + } + + private int CompareProtocol(DownloadDecision x, DownloadDecision y) + { + var result = CompareBy(x.RemoteEpisode, y.RemoteEpisode, remoteEpisode => + { + var delayProfile = _delayProfileService.BestForTags(remoteEpisode.Series.Tags); + var downloadProtocol = remoteEpisode.Release.DownloadProtocol; + return downloadProtocol == delayProfile.PreferredProtocol; + }); + + return result; + } + + private int CompareEpisodeCount(DownloadDecision x, DownloadDecision y) + { + return CompareAll(CompareBy(x.RemoteEpisode, y.RemoteEpisode, remoteEpisode => remoteEpisode.ParsedEpisodeInfo.FullSeason), + CompareByReverse(x.RemoteEpisode, y.RemoteEpisode, remoteEpisode => remoteEpisode.Episodes.Count)); + } + + private int CompareEpisodeNumber(DownloadDecision x, DownloadDecision y) + { + return CompareByReverse(x.RemoteEpisode, y.RemoteEpisode, remoteEpisode => remoteEpisode.Episodes.Select(e => e.EpisodeNumber).MinOrDefault()); + } + + private int ComparePeersIfTorrent(DownloadDecision x, DownloadDecision y) + { + // Different protocols should get caught when checking the preferred protocol, + // since we're dealing with the same series in our comparisions + if (x.RemoteEpisode.Release.DownloadProtocol != DownloadProtocol.Torrent || + y.RemoteEpisode.Release.DownloadProtocol != DownloadProtocol.Torrent) + { + return 0; + } + + return CompareAll( + CompareBy(x.RemoteEpisode, y.RemoteEpisode, remoteEpisode => + { + var seeders = TorrentInfo.GetSeeders(remoteEpisode.Release); + + return seeders.HasValue && seeders.Value > 0 ? Math.Round(Math.Log10(seeders.Value)) : 0; + }), + CompareBy(x.RemoteEpisode, y.RemoteEpisode, remoteEpisode => + { + var peers = TorrentInfo.GetPeers(remoteEpisode.Release); + + return peers.HasValue && peers.Value > 0 ? Math.Round(Math.Log10(peers.Value)) : 0; + })); + } + + private int CompareAgeIfUsenet(DownloadDecision x, DownloadDecision y) + { + if (x.RemoteEpisode.Release.DownloadProtocol != DownloadProtocol.Usenet || + y.RemoteEpisode.Release.DownloadProtocol != DownloadProtocol.Usenet) + { + return 0; + } + + return CompareBy(x.RemoteEpisode, y.RemoteEpisode, remoteEpisode => + { + var ageHours = remoteEpisode.Release.AgeHours; + var age = remoteEpisode.Release.Age; + + if (ageHours < 1) + { + return 1000; + } + + if (ageHours <= 24) + { + return 100; + } + + if (age <= 7) + { + return 10; + } + + return 1; + }); + } + + private int CompareSize(DownloadDecision x, DownloadDecision y) + { + // TODO: Is smaller better? Smaller for usenet could mean no par2 files. + + return CompareBy(x.RemoteEpisode, y.RemoteEpisode, remoteEpisode => remoteEpisode.Release.Size.Round(200.Megabytes())); + } + } +} diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionPriorizationService.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionPriorizationService.cs index c130f0a25..33fc32f5d 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionPriorizationService.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionPriorizationService.cs @@ -1,11 +1,6 @@ -using System; -using System.Linq; +using System.Linq; using System.Collections.Generic; -using NzbDrone.Core.Indexers; -using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Profiles.Delay; -using NzbDrone.Core.Qualities; -using NzbDrone.Core.Tv; namespace NzbDrone.Core.DecisionEngine { @@ -26,35 +21,13 @@ namespace NzbDrone.Core.DecisionEngine public List PrioritizeDecisions(List decisions) { return decisions.Where(c => c.RemoteEpisode.Series != null) - .GroupBy(c => c.RemoteEpisode.Series.Id, (seriesId, d) => + .GroupBy(c => c.RemoteEpisode.Series.Id, (seriesId, downloadDecisions) => { - var downloadDecisions = d.ToList(); - var series = downloadDecisions.First().RemoteEpisode.Series; - - return downloadDecisions - .OrderByDescending(c => c.RemoteEpisode.ParsedEpisodeInfo.Quality, new QualityModelComparer(series.Profile)) - .ThenBy(c => c.RemoteEpisode.Episodes.Select(e => e.EpisodeNumber).MinOrDefault()) - .ThenBy(c => PrioritizeDownloadProtocol(series, c.RemoteEpisode.Release.DownloadProtocol)) - .ThenByDescending(c => c.RemoteEpisode.Episodes.Count) - .ThenBy(c => c.RemoteEpisode.Release.Size.Round(200.Megabytes()) / Math.Max(1, c.RemoteEpisode.Episodes.Count)) - .ThenByDescending(c => TorrentInfo.GetSeeders(c.RemoteEpisode.Release)) - .ThenBy(c => c.RemoteEpisode.Release.Age); + return downloadDecisions.OrderByDescending(decision => decision, new DownloadDecisionComparer(_delayProfileService)); }) .SelectMany(c => c) .Union(decisions.Where(c => c.RemoteEpisode.Series == null)) .ToList(); } - - private int PrioritizeDownloadProtocol(Series series, DownloadProtocol downloadProtocol) - { - var delayProfile = _delayProfileService.BestForTags(series.Tags); - - if (downloadProtocol == delayProfile.PreferredProtocol) - { - return 0; - } - - return 1; - } } } diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index dc77e71a2..bc0ffc2a4 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -298,6 +298,7 @@ + diff --git a/src/NzbDrone.Core/Parser/Model/TorrentInfo.cs b/src/NzbDrone.Core/Parser/Model/TorrentInfo.cs index 5987102cc..126080841 100644 --- a/src/NzbDrone.Core/Parser/Model/TorrentInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/TorrentInfo.cs @@ -18,9 +18,22 @@ namespace NzbDrone.Core.Parser.Model { return null; } + return torrentInfo.Seeders; } + public static int? GetPeers(ReleaseInfo release) + { + var torrentInfo = release as TorrentInfo; + + if (torrentInfo == null) + { + return null; + } + + return torrentInfo.Peers; + } + public override string ToString(string format) { var stringBuilder = new StringBuilder(base.ToString(format));