diff --git a/src/NzbDrone.Api/Blacklist/BlacklistResource.cs b/src/NzbDrone.Api/Blacklist/BlacklistResource.cs index e75ca83f3..e213518e4 100644 --- a/src/NzbDrone.Api/Blacklist/BlacklistResource.cs +++ b/src/NzbDrone.Api/Blacklist/BlacklistResource.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using NzbDrone.Api.REST; using NzbDrone.Core.Qualities; using NzbDrone.Api.Series; +using NzbDrone.Core.Indexers; namespace NzbDrone.Api.Blacklist { @@ -13,6 +14,9 @@ namespace NzbDrone.Api.Blacklist public string SourceTitle { get; set; } public QualityModel Quality { get; set; } public DateTime Date { get; set; } + public DownloadProtocol Protocol { get; set; } + public string Indexer { get; set; } + public string Message { get; set; } public SeriesResource Series { get; set; } } diff --git a/src/NzbDrone.Api/History/HistoryResource.cs b/src/NzbDrone.Api/History/HistoryResource.cs index 6d422f805..3c42e77c6 100644 --- a/src/NzbDrone.Api/History/HistoryResource.cs +++ b/src/NzbDrone.Api/History/HistoryResource.cs @@ -18,7 +18,6 @@ namespace NzbDrone.Api.History public Boolean QualityCutoffNotMet { get; set; } public DateTime Date { get; set; } public string Indexer { get; set; } - public string NzbInfoUrl { get; set; } public string ReleaseGroup { get; set; } public string DownloadId { get; set; } diff --git a/src/NzbDrone.Core.Test/Blacklisting/BlacklistRepositoryFixture.cs b/src/NzbDrone.Core.Test/Blacklisting/BlacklistRepositoryFixture.cs index e4ea99c55..4cc75b955 100644 --- a/src/NzbDrone.Core.Test/Blacklisting/BlacklistRepositoryFixture.cs +++ b/src/NzbDrone.Core.Test/Blacklisting/BlacklistRepositoryFixture.cs @@ -47,7 +47,7 @@ namespace NzbDrone.Core.Test.Blacklisting { Subject.Insert(_blacklist); - Subject.Blacklisted(_blacklist.SeriesId, _blacklist.SourceTitle.ToUpperInvariant()).Should().HaveCount(1); + Subject.BlacklistedByTitle(_blacklist.SeriesId, _blacklist.SourceTitle.ToUpperInvariant()).Should().HaveCount(1); } } } diff --git a/src/NzbDrone.Core.Test/Blacklisting/BlacklistServiceFixture.cs b/src/NzbDrone.Core.Test/Blacklisting/BlacklistServiceFixture.cs index 9f88fd939..8766de661 100644 --- a/src/NzbDrone.Core.Test/Blacklisting/BlacklistServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Blacklisting/BlacklistServiceFixture.cs @@ -28,9 +28,12 @@ namespace NzbDrone.Core.Test.Blacklisting }; _event.Data.Add("publishedDate", DateTime.UtcNow.ToString("s") + "Z"); + _event.Data.Add("size", "1000"); + _event.Data.Add("indexer", "nzbs.org"); + _event.Data.Add("protocol", "1"); + _event.Data.Add("message", "Marked as failed"); } - [Test] public void should_add_to_repository() { @@ -39,5 +42,17 @@ namespace NzbDrone.Core.Test.Blacklisting Mocker.GetMock() .Verify(v => v.Insert(It.Is(b => b.EpisodeIds == _event.EpisodeIds)), Times.Once()); } + + [Test] + public void should_add_to_repository_missing_size_and_protocol() + { + Subject.Handle(_event); + + _event.Data.Remove("size"); + _event.Data.Remove("protocol"); + + Mocker.GetMock() + .Verify(v => v.Insert(It.Is(b => b.EpisodeIds == _event.EpisodeIds)), Times.Once()); + } } } diff --git a/src/NzbDrone.Core/Blacklisting/Blacklist.cs b/src/NzbDrone.Core/Blacklisting/Blacklist.cs index 91c927f05..1c0813ac0 100644 --- a/src/NzbDrone.Core/Blacklisting/Blacklist.cs +++ b/src/NzbDrone.Core/Blacklisting/Blacklist.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using NzbDrone.Core.Datastore; +using NzbDrone.Core.Indexers; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; @@ -8,12 +9,17 @@ namespace NzbDrone.Core.Blacklisting { public class Blacklist : ModelBase { - public Int32 SeriesId { get; set; } + public int SeriesId { get; set; } public Series Series { get; set; } - public List EpisodeIds { get; set; } - public String SourceTitle { get; set; } + public List EpisodeIds { get; set; } + public string SourceTitle { get; set; } public QualityModel Quality { get; set; } public DateTime Date { get; set; } public DateTime? PublishedDate { get; set; } + public long? Size { get; set; } + public DownloadProtocol Protocol { get; set; } + public string Indexer { get; set; } + public string Message { get; set; } + public string TorrentInfoHash { get; set; } } } diff --git a/src/NzbDrone.Core/Blacklisting/BlacklistRepository.cs b/src/NzbDrone.Core/Blacklisting/BlacklistRepository.cs index f6a0ea8a6..906f2a92b 100644 --- a/src/NzbDrone.Core/Blacklisting/BlacklistRepository.cs +++ b/src/NzbDrone.Core/Blacklisting/BlacklistRepository.cs @@ -8,7 +8,8 @@ namespace NzbDrone.Core.Blacklisting { public interface IBlacklistRepository : IBasicRepository { - List Blacklisted(int seriesId, string sourceTitle); + List BlacklistedByTitle(int seriesId, string sourceTitle); + List BlacklistedByTorrentInfoHash(int seriesId, string torrentInfoHash); List BlacklistedBySeries(int seriesId); } @@ -19,12 +20,18 @@ namespace NzbDrone.Core.Blacklisting { } - public List Blacklisted(int seriesId, string sourceTitle) + public List BlacklistedByTitle(int seriesId, string sourceTitle) { return Query.Where(e => e.SeriesId == seriesId) .AndWhere(e => e.SourceTitle.Contains(sourceTitle)); } + public List BlacklistedByTorrentInfoHash(int seriesId, string torrentInfoHash) + { + return Query.Where(e => e.SeriesId == seriesId) + .AndWhere(e => e.TorrentInfoHash.Contains(torrentInfoHash)); + } + public List BlacklistedBySeries(int seriesId) { return Query.Where(b => b.SeriesId == seriesId); diff --git a/src/NzbDrone.Core/Blacklisting/BlacklistService.cs b/src/NzbDrone.Core/Blacklisting/BlacklistService.cs index f72a349c5..c49dc19e1 100644 --- a/src/NzbDrone.Core/Blacklisting/BlacklistService.cs +++ b/src/NzbDrone.Core/Blacklisting/BlacklistService.cs @@ -3,20 +3,22 @@ using System.Linq; using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore; using NzbDrone.Core.Download; +using NzbDrone.Core.Indexers; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Tv.Events; namespace NzbDrone.Core.Blacklisting { public interface IBlacklistService { - bool Blacklisted(int seriesId, string sourceTitle, DateTime publishedDate); + bool Blacklisted(int seriesId, ReleaseInfo release); PagingSpec Paged(PagingSpec pagingSpec); void Delete(int id); } - public class BlacklistService : IBlacklistService, + IExecute, IHandle, IHandleAsync @@ -28,11 +30,29 @@ namespace NzbDrone.Core.Blacklisting _blacklistRepository = blacklistRepository; } - public bool Blacklisted(int seriesId, string sourceTitle, DateTime publishedDate) + public bool Blacklisted(int seriesId, ReleaseInfo release) { - var blacklisted = _blacklistRepository.Blacklisted(seriesId, sourceTitle); + var blacklistedByTitle = _blacklistRepository.BlacklistedByTitle(seriesId, release.Title); + + if (release.DownloadProtocol == DownloadProtocol.Torrent) + { + var torrentInfo = release as TorrentInfo; + + if (torrentInfo == null) return false; + + if (torrentInfo.InfoHash.IsNullOrWhiteSpace()) + { + return blacklistedByTitle.Where(b => b.Protocol == DownloadProtocol.Torrent) + .Any(b => SameTorrent(b, torrentInfo)); + } + + var blacklistedByTorrentInfohash = _blacklistRepository.BlacklistedByTitle(seriesId, torrentInfo.InfoHash); + + return blacklistedByTorrentInfohash.Any(b => SameTorrent(b, torrentInfo)); + } - return blacklisted.Any(item => HasSamePublishedDate(item, publishedDate)); + return blacklistedByTitle.Where(b => b.Protocol == DownloadProtocol.Usenet) + .Any(b => SameNzb(b, release)); } public PagingSpec Paged(PagingSpec pagingSpec) @@ -45,12 +65,58 @@ namespace NzbDrone.Core.Blacklisting _blacklistRepository.Delete(id); } - private static bool HasSamePublishedDate(Blacklist item, DateTime publishedDate) + private bool SameNzb(Blacklist item, ReleaseInfo release) + { + if (item.PublishedDate == release.PublishDate) + { + return true; + } + + if (!HasSameIndexer(item, release.Indexer) && + HasSamePublishedDate(item, release.PublishDate) && + HasSameSize(item, release.Size)) + { + return true; + } + + return false; + } + + private bool SameTorrent(Blacklist item, TorrentInfo release) + { + if (release.InfoHash.IsNotNullOrWhiteSpace()) + { + return release.InfoHash.Equals(item.TorrentInfoHash); + } + + return item.Indexer.Equals(release.Indexer, StringComparison.InvariantCultureIgnoreCase); + } + + private bool HasSameIndexer(Blacklist item, string indexer) + { + if (item.Indexer.IsNullOrWhiteSpace()) + { + return true; + } + + return item.Indexer.Equals(indexer, StringComparison.InvariantCultureIgnoreCase); + } + + private bool HasSamePublishedDate(Blacklist item, DateTime publishedDate) { if (!item.PublishedDate.HasValue) return true; - return item.PublishedDate.Value.AddDays(-2) <= publishedDate && - item.PublishedDate.Value.AddDays(2) >= publishedDate; + return item.PublishedDate.Value.AddMinutes(-2) <= publishedDate && + item.PublishedDate.Value.AddMinutes(2) >= publishedDate; + } + + private bool HasSameSize(Blacklist item, long size) + { + if (!item.Size.HasValue) return true; + + var difference = Math.Abs(item.Size.Value - size); + + return difference <= 2.Megabytes(); } public void Execute(ClearBlacklistCommand message) @@ -67,7 +133,12 @@ namespace NzbDrone.Core.Blacklisting SourceTitle = message.SourceTitle, Quality = message.Quality, Date = DateTime.UtcNow, - PublishedDate = DateTime.Parse(message.Data.GetValueOrDefault("publishedDate")) + PublishedDate = DateTime.Parse(message.Data.GetValueOrDefault("publishedDate")), + Size = Int64.Parse(message.Data.GetValueOrDefault("size", "0")), + Indexer = message.Data.GetValueOrDefault("indexer"), + Protocol = (DownloadProtocol)Convert.ToInt32(message.Data.GetValueOrDefault("protocol")), + Message = message.Message, + TorrentInfoHash = message.Data.GetValueOrDefault("torrentInfoHash") }; _blacklistRepository.Insert(blacklist); diff --git a/src/NzbDrone.Core/Datastore/Migration/083_additonal_blacklist_columns.cs b/src/NzbDrone.Core/Datastore/Migration/083_additonal_blacklist_columns.cs new file mode 100644 index 000000000..93fa2fd51 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/083_additonal_blacklist_columns.cs @@ -0,0 +1,22 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(83)] + public class additonal_blacklist_columns : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("Blacklist").AddColumn("Size").AsInt64().Nullable(); + Alter.Table("Blacklist").AddColumn("Protocol").AsInt32().Nullable(); + Alter.Table("Blacklist").AddColumn("Indexer").AsString().Nullable(); + Alter.Table("Blacklist").AddColumn("Message").AsString().Nullable(); + Alter.Table("Blacklist").AddColumn("TorrentInfoHash").AsString().Nullable(); + + Update.Table("Blacklist") + .Set(new { Protocol = 1 }) + .AllRows(); + } + } +} diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/BlacklistSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/BlacklistSpecification.cs index 0bb86450e..d3b1bd523 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/BlacklistSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/BlacklistSpecification.cs @@ -20,14 +20,8 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public RejectionType Type { get { return RejectionType.Permanent; } } public Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) - { - if (subject.Release.DownloadProtocol == DownloadProtocol.Torrent) - { - return Decision.Accept(); - } - - - if (_blacklistService.Blacklisted(subject.Series.Id, subject.Release.Title, subject.Release.PublishDate)) + { + if (_blacklistService.Blacklisted(subject.Series.Id, subject.Release)) { _logger.Debug("{0} is blacklisted, rejecting.", subject.Release.Title); return Decision.Reject("Release is blacklisted"); diff --git a/src/NzbDrone.Core/History/HistoryService.cs b/src/NzbDrone.Core/History/HistoryService.cs index 5ef06aa1a..4fee5e753 100644 --- a/src/NzbDrone.Core/History/HistoryService.cs +++ b/src/NzbDrone.Core/History/HistoryService.cs @@ -6,9 +6,11 @@ using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore; using NzbDrone.Core.Download; +using NzbDrone.Core.Indexers; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Profiles; using NzbDrone.Core.Qualities; @@ -78,6 +80,51 @@ namespace NzbDrone.Core.History .FirstOrDefault(); } + private string FindDownloadId(EpisodeImportedEvent trackedDownload) + { + _logger.Debug("Trying to find downloadId for {0} from history", trackedDownload.ImportedEpisode.Path); + + var episodeIds = trackedDownload.EpisodeInfo.Episodes.Select(c => c.Id).ToList(); + + var allHistory = _historyRepository.FindDownloadHistory(trackedDownload.EpisodeInfo.Series.Id, trackedDownload.ImportedEpisode.Quality); + + + //Find download related items for these episdoes + var episodesHistory = allHistory.Where(h => episodeIds.Contains(h.EpisodeId)).ToList(); + + var processedDownloadId = episodesHistory + .Where(c => c.EventType != HistoryEventType.Grabbed && c.DownloadId != null) + .Select(c => c.DownloadId); + + var stillDownloading = episodesHistory.Where(c => c.EventType == HistoryEventType.Grabbed && !processedDownloadId.Contains(c.DownloadId)).ToList(); + + string downloadId = null; + + if (stillDownloading.Any()) + { + foreach (var matchingHistory in trackedDownload.EpisodeInfo.Episodes.Select(e => stillDownloading.Where(c => c.EpisodeId == e.Id).ToList())) + { + if (matchingHistory.Count != 1) + { + return null; + } + + var newDownloadId = matchingHistory.Single().DownloadId; + + if (downloadId == null || downloadId == newDownloadId) + { + downloadId = newDownloadId; + } + else + { + return null; + } + } + } + + return downloadId; + } + public void Handle(EpisodeGrabbedEvent message) { foreach (var episode in message.Episode.Episodes) @@ -101,12 +148,24 @@ namespace NzbDrone.Core.History history.Data.Add("AgeMinutes", message.Episode.Release.AgeMinutes.ToString()); history.Data.Add("PublishedDate", message.Episode.Release.PublishDate.ToString("s") + "Z"); history.Data.Add("DownloadClient", message.DownloadClient); + history.Data.Add("Size", message.Episode.Release.Size.ToString()); + history.Data.Add("DownloadUrl", message.Episode.Release.DownloadUrl); + history.Data.Add("Guid", message.Episode.Release.Guid); + history.Data.Add("TvRageId", message.Episode.Release.TvRageId.ToString()); + history.Data.Add("Protocol", ((int)message.Episode.Release.DownloadProtocol).ToString()); if (!message.Episode.ParsedEpisodeInfo.ReleaseHash.IsNullOrWhiteSpace()) { history.Data.Add("ReleaseHash", message.Episode.ParsedEpisodeInfo.ReleaseHash); } + var torrentRelease = message.Episode.Release as TorrentInfo; + + if (torrentRelease != null) + { + history.Data.Add("TorrentInfoHash", torrentRelease.InfoHash); + } + _historyRepository.Insert(history); } } @@ -148,52 +207,6 @@ namespace NzbDrone.Core.History } } - - private string FindDownloadId(EpisodeImportedEvent trackedDownload) - { - _logger.Debug("Trying to find downloadId for {0} from history", trackedDownload.ImportedEpisode.Path); - - var episodeIds = trackedDownload.EpisodeInfo.Episodes.Select(c => c.Id).ToList(); - - var allHistory = _historyRepository.FindDownloadHistory(trackedDownload.EpisodeInfo.Series.Id, trackedDownload.ImportedEpisode.Quality); - - - //Find download related items for these episdoes - var episodesHistory = allHistory.Where(h => episodeIds.Contains(h.EpisodeId)).ToList(); - - var processedDownloadId = episodesHistory - .Where(c => c.EventType != HistoryEventType.Grabbed && c.DownloadId != null) - .Select(c => c.DownloadId); - - var stillDownloading = episodesHistory.Where(c => c.EventType == HistoryEventType.Grabbed && !processedDownloadId.Contains(c.DownloadId)).ToList(); - - string downloadId = null; - - if (stillDownloading.Any()) - { - foreach (var matchingHistory in trackedDownload.EpisodeInfo.Episodes.Select(e => stillDownloading.Where(c => c.EpisodeId == e.Id).ToList())) - { - if (matchingHistory.Count != 1) - { - return null; - } - - var newDownloadId = matchingHistory.Single().DownloadId; - - if (downloadId == null || downloadId == newDownloadId) - { - downloadId = newDownloadId; - } - else - { - return null; - } - } - } - - return downloadId; - } - public void Handle(DownloadFailedEvent message) { foreach (var episodeId in message.EpisodeIds) diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index e52a5153f..9c67b5506 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -254,6 +254,7 @@ + diff --git a/src/UI/Activity/Blacklist/BlacklistActionsCell.js b/src/UI/Activity/Blacklist/BlacklistActionsCell.js index beb48fce8..61ce7d102 100644 --- a/src/UI/Activity/Blacklist/BlacklistActionsCell.js +++ b/src/UI/Activity/Blacklist/BlacklistActionsCell.js @@ -1,19 +1,27 @@ +var vent = require('vent'); var NzbDroneCell = require('../../Cells/NzbDroneCell'); +var BlacklistDetailsLayout = require('./Details/BlacklistDetailsLayout'); module.exports = NzbDroneCell.extend({ - className : 'blacklist-controls-cell', + className : 'blacklist-actions-cell', events : { - 'click' : '_delete' + 'click .x-details' : '_details', + 'click .x-delete' : '_delete' }, render : function() { this.$el.empty(); - this.$el.html(''); + this.$el.html('' + + ''); return this; }, + _details : function() { + vent.trigger(vent.Commands.OpenModalCommand, new BlacklistDetailsLayout({ model : this.model })); + }, + _delete : function() { this.model.destroy(); } diff --git a/src/UI/Activity/Blacklist/Details/BlacklistDetailsLayout.js b/src/UI/Activity/Blacklist/Details/BlacklistDetailsLayout.js new file mode 100644 index 000000000..cdcbf25f0 --- /dev/null +++ b/src/UI/Activity/Blacklist/Details/BlacklistDetailsLayout.js @@ -0,0 +1,14 @@ +var Marionette = require('marionette'); +var BlacklistDetailsView = require('./BlacklistDetailsView'); + +module.exports = Marionette.Layout.extend({ + template : 'Activity/Blacklist/Details/BlacklistDetailsLayoutTemplate', + + regions : { + bodyRegion : '.modal-body' + }, + + onShow : function() { + this.bodyRegion.show(new BlacklistDetailsView({ model : this.model })); + } +}); \ No newline at end of file diff --git a/src/UI/Activity/Blacklist/Details/BlacklistDetailsLayoutTemplate.hbs b/src/UI/Activity/Blacklist/Details/BlacklistDetailsLayoutTemplate.hbs new file mode 100644 index 000000000..b62d60341 --- /dev/null +++ b/src/UI/Activity/Blacklist/Details/BlacklistDetailsLayoutTemplate.hbs @@ -0,0 +1,18 @@ + diff --git a/src/UI/Activity/Blacklist/Details/BlacklistDetailsView.js b/src/UI/Activity/Blacklist/Details/BlacklistDetailsView.js new file mode 100644 index 000000000..1b7bc883d --- /dev/null +++ b/src/UI/Activity/Blacklist/Details/BlacklistDetailsView.js @@ -0,0 +1,5 @@ +var Marionette = require('marionette'); + +module.exports = Marionette.ItemView.extend({ + template : 'Activity/Blacklist/Details/BlacklistDetailsViewTemplate' +}); \ No newline at end of file diff --git a/src/UI/Activity/Blacklist/Details/BlacklistDetailsViewTemplate.hbs b/src/UI/Activity/Blacklist/Details/BlacklistDetailsViewTemplate.hbs new file mode 100644 index 000000000..d29a878fc --- /dev/null +++ b/src/UI/Activity/Blacklist/Details/BlacklistDetailsViewTemplate.hbs @@ -0,0 +1,23 @@ +
+ +
Name:
+
{{sourceTitle}}
+ + {{#if protocol}} + {{#unless_eq protocol compare="unknown"}} +
Protocol:
+
{{protocol}}
+ {{/unless_eq}} + {{/if}} + + {{#if indexer}} +
Indexer:
+
{{indexer}}
+ {{/if}} + + + {{#if message}} +
Message:
+
{{message}}
+ {{/if}} +
diff --git a/src/UI/Cells/cells.less b/src/UI/Cells/cells.less index 03ad38911..b78da6c1a 100644 --- a/src/UI/Cells/cells.less +++ b/src/UI/Cells/cells.less @@ -236,3 +236,15 @@ td.delete-episode-file-cell { .age-cell { cursor : default; } + +.blacklist-actions-cell { + min-width : 55px; + width : 55px; + text-align : right !important; + + i { + .clickable(); + margin-left : 2px; + margin-right : 2px; + } +}