using System; using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; using NzbDrone.Core.Blocklisting; using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Download; using NzbDrone.Core.Download.Pending; using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.Languages; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Profiles.Languages; using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.Queue; using NzbDrone.SignalR; using Sonarr.Http; using Sonarr.Http.Extensions; using Sonarr.Http.REST; using Sonarr.Http.REST.Attributes; namespace Sonarr.Api.V3.Queue { [V3ApiController] public class QueueController : RestControllerWithSignalR, IHandle, IHandle { private readonly IQueueService _queueService; private readonly IPendingReleaseService _pendingReleaseService; private readonly LanguageComparer _languageComparer; private readonly QualityModelComparer _qualityComparer; private readonly ITrackedDownloadService _trackedDownloadService; private readonly IFailedDownloadService _failedDownloadService; private readonly IIgnoredDownloadService _ignoredDownloadService; private readonly IProvideDownloadClient _downloadClientProvider; private readonly IBlocklistService _blocklistService; public QueueController(IBroadcastSignalRMessage broadcastSignalRMessage, IQueueService queueService, IPendingReleaseService pendingReleaseService, IQualityProfileService qualityProfileService, ILanguageProfileService languageProfileService, ITrackedDownloadService trackedDownloadService, IFailedDownloadService failedDownloadService, IIgnoredDownloadService ignoredDownloadService, IProvideDownloadClient downloadClientProvider, IBlocklistService blocklistService) : base(broadcastSignalRMessage) { _queueService = queueService; _pendingReleaseService = pendingReleaseService; _trackedDownloadService = trackedDownloadService; _failedDownloadService = failedDownloadService; _ignoredDownloadService = ignoredDownloadService; _downloadClientProvider = downloadClientProvider; _blocklistService = blocklistService; _qualityComparer = new QualityModelComparer(qualityProfileService.GetDefaultProfile(string.Empty)); _languageComparer = new LanguageComparer(languageProfileService.GetDefaultProfile(string.Empty)); } protected override QueueResource GetResourceById(int id) { throw new NotImplementedException(); } [RestDeleteById] public void RemoveAction(int id, bool removeFromClient = true, bool blocklist = false) { var trackedDownload = Remove(id, removeFromClient, blocklist); if (trackedDownload != null) { _trackedDownloadService.StopTracking(trackedDownload.DownloadItem.DownloadId); } } [HttpDelete("bulk")] public object RemoveMany([FromBody] QueueBulkResource resource, [FromQuery] bool removeFromClient = true, [FromQuery] bool blocklist = false) { var trackedDownloadIds = new List(); foreach (var id in resource.Ids) { var trackedDownload = Remove(id, removeFromClient, blocklist); if (trackedDownload != null) { trackedDownloadIds.Add(trackedDownload.DownloadItem.DownloadId); } } _trackedDownloadService.StopTracking(trackedDownloadIds); return new { }; } [HttpGet] [Produces("application/json")] public PagingResource GetQueue(bool includeUnknownSeriesItems = false, bool includeSeries = false, bool includeEpisode = false) { var pagingResource = Request.ReadPagingResourceFromRequest(); var pagingSpec = pagingResource.MapToPagingSpec("timeleft", SortDirection.Ascending); return pagingSpec.ApplyToPage((spec) => GetQueue(spec, includeUnknownSeriesItems), (q) => MapToResource(q, includeSeries, includeEpisode)); } private PagingSpec GetQueue(PagingSpec pagingSpec, bool includeUnknownSeriesItems) { var ascending = pagingSpec.SortDirection == SortDirection.Ascending; var orderByFunc = GetOrderByFunc(pagingSpec); var queue = _queueService.GetQueue(); var filteredQueue = includeUnknownSeriesItems ? queue : queue.Where(q => q.Series != null); var pending = _pendingReleaseService.GetPendingQueue(); var fullQueue = filteredQueue.Concat(pending).ToList(); IOrderedEnumerable ordered; if (pagingSpec.SortKey == "timeleft") { ordered = ascending ? fullQueue.OrderBy(q => q.Timeleft, new TimeleftComparer()) : fullQueue.OrderByDescending(q => q.Timeleft, new TimeleftComparer()); } else if (pagingSpec.SortKey == "estimatedCompletionTime") { ordered = ascending ? fullQueue.OrderBy(q => q.EstimatedCompletionTime, new EstimatedCompletionTimeComparer()) : fullQueue.OrderByDescending(q => q.EstimatedCompletionTime, new EstimatedCompletionTimeComparer()); } else if (pagingSpec.SortKey == "protocol") { ordered = ascending ? fullQueue.OrderBy(q => q.Protocol) : fullQueue.OrderByDescending(q => q.Protocol); } else if (pagingSpec.SortKey == "indexer") { ordered = ascending ? fullQueue.OrderBy(q => q.Indexer, StringComparer.InvariantCultureIgnoreCase) : fullQueue.OrderByDescending(q => q.Indexer, StringComparer.InvariantCultureIgnoreCase); } else if (pagingSpec.SortKey == "downloadClient") { ordered = ascending ? fullQueue.OrderBy(q => q.DownloadClient, StringComparer.InvariantCultureIgnoreCase) : fullQueue.OrderByDescending(q => q.DownloadClient, StringComparer.InvariantCultureIgnoreCase); } else if (pagingSpec.SortKey == "quality") { ordered = ascending ? fullQueue.OrderBy(q => q.Quality, _qualityComparer) : fullQueue.OrderByDescending(q => q.Quality, _qualityComparer); } else if (pagingSpec.SortKey == "language") { ordered = ascending ? fullQueue.OrderBy(q => q.Language, _languageComparer) : fullQueue.OrderByDescending(q => q.Language, _languageComparer); } else { ordered = ascending ? fullQueue.OrderBy(orderByFunc) : fullQueue.OrderByDescending(orderByFunc); } ordered = ordered.ThenByDescending(q => q.Size == 0 ? 0 : 100 - (q.Sizeleft / q.Size * 100)); pagingSpec.Records = ordered.Skip((pagingSpec.Page - 1) * pagingSpec.PageSize).Take(pagingSpec.PageSize).ToList(); pagingSpec.TotalRecords = fullQueue.Count; if (pagingSpec.Records.Empty() && pagingSpec.Page > 1) { pagingSpec.Page = (int)Math.Max(Math.Ceiling((decimal)(pagingSpec.TotalRecords / pagingSpec.PageSize)), 1); pagingSpec.Records = ordered.Skip((pagingSpec.Page - 1) * pagingSpec.PageSize).Take(pagingSpec.PageSize).ToList(); } return pagingSpec; } private Func GetOrderByFunc(PagingSpec pagingSpec) { switch (pagingSpec.SortKey) { case "status": return q => q.Status; case "series.sortTitle": return q => q.Series?.SortTitle ?? string.Empty; case "title": return q => q.Title; case "episode": return q => q.Episode; case "episodes.airDateUtc": return q => q.Episode?.AirDateUtc ?? DateTime.MinValue; case "episodes.title": return q => q.Episode?.Title ?? string.Empty; case "language": return q => q.Language; case "quality": return q => q.Quality; case "progress": // Avoid exploding if a download's size is 0 return q => 100 - (q.Sizeleft / Math.Max(q.Size * 100, 1)); default: return q => q.Timeleft; } } private TrackedDownload Remove(int id, bool removeFromClient, bool blocklist) { var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id); if (pendingRelease != null) { _blocklistService.Block(pendingRelease.RemoteEpisode, "Pending release manually blocklisted"); _pendingReleaseService.RemovePendingQueueItems(pendingRelease.Id); return null; } var trackedDownload = GetTrackedDownload(id); if (trackedDownload == null) { throw new NotFoundException(); } if (removeFromClient) { var downloadClient = _downloadClientProvider.Get(trackedDownload.DownloadClient); if (downloadClient == null) { throw new BadRequestException(); } downloadClient.RemoveItem(trackedDownload.DownloadItem, true); } if (blocklist) { _failedDownloadService.MarkAsFailed(trackedDownload.DownloadItem.DownloadId); } if (!removeFromClient && !blocklist) { if (!_ignoredDownloadService.IgnoreDownload(trackedDownload)) { return null; } } return trackedDownload; } private TrackedDownload GetTrackedDownload(int queueId) { var queueItem = _queueService.Find(queueId); if (queueItem == null) { throw new NotFoundException(); } var trackedDownload = _trackedDownloadService.Find(queueItem.DownloadId); if (trackedDownload == null) { throw new NotFoundException(); } return trackedDownload; } private QueueResource MapToResource(NzbDrone.Core.Queue.Queue queueItem, bool includeSeries, bool includeEpisode) { return queueItem.ToResource(includeSeries, includeEpisode); } [NonAction] public void Handle(QueueUpdatedEvent message) { BroadcastResourceChange(ModelAction.Sync); } [NonAction] public void Handle(PendingReleasesUpdatedEvent message) { BroadcastResourceChange(ModelAction.Sync); } } }