using System; using System.Collections.Generic; using System.Linq; using NLog; using NzbDrone.Common.Crypto; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Indexers; using NzbDrone.Core.Jobs; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Profiles.Delay; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; using NzbDrone.Core.Tv.Events; namespace NzbDrone.Core.Download.Pending { public interface IPendingReleaseService { void Add(DownloadDecision decision); List GetPending(); List GetPendingRemoteMovies(int movieId); List GetPendingQueue(); Queue.Queue FindPendingQueueItem(int queueId); void RemovePendingQueueItems(int queueId); RemoteMovie OldestPendingRelease(int movieId); } public class PendingReleaseService : IPendingReleaseService, IHandle, IHandle, IHandle { private readonly IIndexerStatusService _indexerStatusService; private readonly IPendingReleaseRepository _repository; private readonly IMovieService _movieService; private readonly IParsingService _parsingService; private readonly IDelayProfileService _delayProfileService; private readonly ITaskManager _taskManager; private readonly IConfigService _configService; private readonly IEventAggregator _eventAggregator; private readonly Logger _logger; public PendingReleaseService(IIndexerStatusService indexerStatusService, IPendingReleaseRepository repository, IMovieService movieService, IParsingService parsingService, IDelayProfileService delayProfileService, ITaskManager taskManager, IConfigService configService, IEventAggregator eventAggregator, Logger logger) { _indexerStatusService = indexerStatusService; _repository = repository; _movieService = movieService; _parsingService = parsingService; _delayProfileService = delayProfileService; _taskManager = taskManager; _configService = configService; _eventAggregator = eventAggregator; _logger = logger; } public void Add(DownloadDecision decision) { var alreadyPending = GetPendingReleases(); var movieId = decision.RemoteMovie.Movie.Id; var existingReports = alreadyPending.Where(r => r.RemoteMovie.Movie.Id == movieId); if (existingReports.Any(MatchingReleasePredicate(decision.RemoteMovie.Release))) { _logger.Debug("This release is already pending, not adding again"); return; } _logger.Debug("Adding release to pending releases"); Insert(decision); } public List GetPending() { var releases = _repository.All().Select(p => p.Release).ToList(); if (releases.Any()) { releases = FilterBlockedIndexers(releases); } return releases; } private List FilterBlockedIndexers(List releases) { var blockedIndexers = new HashSet(_indexerStatusService.GetBlockedIndexers().Select(v => v.IndexerId)); return releases.Where(release => !blockedIndexers.Contains(release.IndexerId)).ToList(); } public List GetPendingRemoteMovies(int movieId) { return _repository.AllByMovieId(movieId).Select(GetRemoteMovie).ToList(); } public List GetPendingQueue() { var queued = new List(); var nextRssSync = new Lazy(() => _taskManager.GetNextExecution(typeof(RssSyncCommand))); foreach (var pendingRelease in GetPendingReleases()) { var ect = pendingRelease.Release.PublishDate.AddMinutes(GetDelay(pendingRelease.RemoteMovie)); if (ect < nextRssSync.Value) { ect = nextRssSync.Value; } else { ect = ect.AddMinutes(_configService.RssSyncInterval); } var queue = new Queue.Queue { Id = GetQueueId(pendingRelease, pendingRelease.RemoteMovie.Movie), Series = null, Episode = null, Movie = pendingRelease.RemoteMovie.Movie, Quality = pendingRelease.RemoteMovie.ParsedMovieInfo?.Quality ?? new QualityModel(), Title = pendingRelease.Title, Size = pendingRelease.RemoteMovie.Release.Size, Sizeleft = pendingRelease.RemoteMovie.Release.Size, RemoteMovie = pendingRelease.RemoteMovie, Timeleft = ect.Subtract(DateTime.UtcNow), EstimatedCompletionTime = ect, Status = "Pending", Protocol = pendingRelease.RemoteMovie.Release.DownloadProtocol }; queued.Add(queue); } //Return best quality release for each episode var deduped = queued.GroupBy(q => q.Movie.Id).Select(g => { var movies = g.First().Movie; return g.OrderByDescending(e => e.Quality, new QualityModelComparer(movies.Profile)) .ThenBy(q => PrioritizeDownloadProtocol(q.Movie, q.Protocol)) .First(); }); return deduped.ToList(); } public Queue.Queue FindPendingQueueItem(int queueId) { return GetPendingQueue().SingleOrDefault(p => p.Id == queueId); } public void RemovePendingQueueItems(int queueId) { var targetItem = FindPendingRelease(queueId); var movieReleases = _repository.AllByMovieId(targetItem.MovieId); var releasesToRemove = movieReleases.Where(c => c.ParsedMovieInfo.MovieTitle == targetItem.ParsedMovieInfo.MovieTitle); _repository.DeleteMany(releasesToRemove.Select(c => c.Id)); } public RemoteMovie OldestPendingRelease(int movieId) { return GetPendingRemoteMovies(movieId).OrderByDescending(p => p.Release.AgeHours).FirstOrDefault(); } private List GetPendingReleases() { var result = new List(); foreach (var release in _repository.All()) { var remoteMovie = GetRemoteMovie(release); if (remoteMovie == null) continue; release.RemoteMovie = remoteMovie; result.Add(release); } return result; } private RemoteMovie GetRemoteMovie(PendingRelease release) { var movie = _movieService.GetMovie(release.MovieId); //Just in case the series was removed, but wasn't cleaned up yet (housekeeper will clean it up) if (movie == null) return null; // var episodes = _parsingService.GetMovie(release.ParsedMovieInfo.MovieTitle); return new RemoteMovie { Movie = movie, ParsedMovieInfo = release.ParsedMovieInfo, Release = release.Release }; } private void Insert(DownloadDecision decision) { var release = new PendingRelease { MovieId = decision.RemoteMovie.Movie.Id, ParsedMovieInfo = decision.RemoteMovie.ParsedMovieInfo, Release = decision.RemoteMovie.Release, Title = decision.RemoteMovie.Release.Title, Added = DateTime.UtcNow }; _repository.Insert(release); _eventAggregator.PublishEvent(new PendingReleasesUpdatedEvent()); } private void Delete(PendingRelease pendingRelease) { _repository.Delete(pendingRelease); _eventAggregator.PublishEvent(new PendingReleasesUpdatedEvent()); } private static Func MatchingReleasePredicate(ReleaseInfo release) { return p => p.Title == release.Title && p.Release.PublishDate == release.PublishDate && p.Release.Indexer == release.Indexer; } private int GetDelay(RemoteMovie remoteMovie) { var delayProfile = _delayProfileService.AllForTags(remoteMovie.Movie.Tags).OrderBy(d => d.Order).First(); var delay = delayProfile.GetProtocolDelay(remoteMovie.Release.DownloadProtocol); var minimumAge = _configService.MinimumAge; return new[] { delay, minimumAge }.Max(); } private void RemoveGrabbed(RemoteMovie remoteMovie) { var pendingReleases = GetPendingReleases(); var existingReports = pendingReleases.Where(r => r.RemoteMovie.Movie.Id == remoteMovie.Movie.Id) .ToList(); if (existingReports.Empty()) { return; } var profile = remoteMovie.Movie.Profile.Value; foreach (var existingReport in existingReports) { var compare = new QualityModelComparer(profile).Compare(remoteMovie.ParsedMovieInfo.Quality, existingReport.RemoteMovie.ParsedMovieInfo.Quality); //Only remove lower/equal quality pending releases //It is safer to retry these releases on the next round than remove it and try to re-add it (if its still in the feed) if (compare >= 0) { _logger.Debug("Removing previously pending release, as it was grabbed."); Delete(existingReport); } } } private void RemoveRejected(List rejected) { _logger.Debug("Removing failed releases from pending"); var pending = GetPendingReleases(); foreach (var rejectedRelease in rejected) { var matching = pending.Where(MatchingReleasePredicate(rejectedRelease.RemoteMovie.Release)); foreach (var pendingRelease in matching) { _logger.Debug("Removing previously pending release, as it has now been rejected."); Delete(pendingRelease); } } } private PendingRelease FindPendingRelease(int queueId) { return GetPendingReleases().First(p => queueId == GetQueueId(p, p.RemoteMovie.Movie)); } private int GetQueueId(PendingRelease pendingRelease, Movie movie) { return HashConverter.GetHashInt31(string.Format("pending-{0}-movie{1}", pendingRelease.Id, movie.Id)); } private int PrioritizeDownloadProtocol(Movie movie, DownloadProtocol downloadProtocol) { var delayProfile = _delayProfileService.BestForTags(movie.Tags); if (downloadProtocol == delayProfile.PreferredProtocol) { return 0; } return 1; } public void Handle(MovieDeletedEvent message) { _repository.DeleteByMovieId(message.Movie.Id); } public void Handle(MovieGrabbedEvent message) { RemoveGrabbed(message.Movie); } public void Handle(RssSyncCompleteEvent message) { RemoveRejected(message.ProcessedDecisions.Rejected); } } }