using System; using System.Collections.Generic; using System.Linq; using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Extensions; using NzbDrone.Common.Serializer; using NzbDrone.Core.Books; using NzbDrone.Core.Books.Events; using NzbDrone.Core.CustomFormats; using NzbDrone.Core.Download.History; using NzbDrone.Core.History; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.Download.TrackedDownloads { public interface ITrackedDownloadService { TrackedDownload Find(string downloadId); void StopTracking(string downloadId); void StopTracking(List downloadIds); TrackedDownload TrackDownload(DownloadClientDefinition downloadClient, DownloadClientItem downloadItem); List GetTrackedDownloads(); void UpdateTrackable(List trackedDownloads); } public class TrackedDownloadService : ITrackedDownloadService, IHandle, IHandle { private readonly IParsingService _parsingService; private readonly IHistoryService _historyService; private readonly IEventAggregator _eventAggregator; private readonly IDownloadHistoryService _downloadHistoryService; private readonly ICustomFormatCalculationService _formatCalculator; private readonly Logger _logger; private readonly ICached _cache; public TrackedDownloadService(IParsingService parsingService, ICacheManager cacheManager, IHistoryService historyService, IEventAggregator eventAggregator, IDownloadHistoryService downloadHistoryService, ICustomFormatCalculationService formatCalculator, Logger logger) { _parsingService = parsingService; _historyService = historyService; _cache = cacheManager.GetCache(GetType()); _formatCalculator = formatCalculator; _eventAggregator = eventAggregator; _downloadHistoryService = downloadHistoryService; _cache = cacheManager.GetCache(GetType()); _logger = logger; } public TrackedDownload Find(string downloadId) { return _cache.Find(downloadId); } public void UpdateBookCache(int bookId) { var updateCacheItems = _cache.Values.Where(x => x.RemoteBook != null && x.RemoteBook.Books.Any(a => a.Id == bookId)).ToList(); if (updateCacheItems.Any()) { foreach (var item in updateCacheItems) { var parsedBookInfo = Parser.Parser.ParseBookTitle(item.DownloadItem.Title); item.RemoteBook = null; if (parsedBookInfo != null) { item.RemoteBook = _parsingService.Map(parsedBookInfo); } } _eventAggregator.PublishEvent(new TrackedDownloadRefreshedEvent(GetTrackedDownloads())); } } public void StopTracking(string downloadId) { var trackedDownload = _cache.Find(downloadId); _cache.Remove(downloadId); _eventAggregator.PublishEvent(new TrackedDownloadsRemovedEvent(new List { trackedDownload })); } public void StopTracking(List downloadIds) { var trackedDownloads = new List(); foreach (var downloadId in downloadIds) { var trackedDownload = _cache.Find(downloadId); _cache.Remove(downloadId); trackedDownloads.Add(trackedDownload); } _eventAggregator.PublishEvent(new TrackedDownloadsRemovedEvent(trackedDownloads)); } public TrackedDownload TrackDownload(DownloadClientDefinition downloadClient, DownloadClientItem downloadItem) { var existingItem = Find(downloadItem.DownloadId); if (existingItem != null && existingItem.State != TrackedDownloadState.Downloading) { LogItemChange(existingItem, existingItem.DownloadItem, downloadItem); existingItem.DownloadItem = downloadItem; existingItem.IsTrackable = true; return existingItem; } var trackedDownload = new TrackedDownload { DownloadClient = downloadClient.Id, DownloadItem = downloadItem, Protocol = downloadClient.Protocol, IsTrackable = true }; try { var parsedBookInfo = Parser.Parser.ParseBookTitle(trackedDownload.DownloadItem.Title); var historyItems = _historyService.FindByDownloadId(downloadItem.DownloadId) .OrderByDescending(h => h.Date) .ToList(); if (parsedBookInfo != null) { trackedDownload.RemoteBook = _parsingService.Map(parsedBookInfo); } var downloadHistory = _downloadHistoryService.GetLatestDownloadHistoryItem(downloadItem.DownloadId); if (downloadHistory != null) { var state = GetStateFromHistory(downloadHistory.EventType); trackedDownload.State = state; if (downloadHistory.EventType == DownloadHistoryEventType.DownloadImportIncomplete) { var messages = Json.Deserialize>(downloadHistory.Data["statusMessages"]).ToArray(); trackedDownload.Warn(messages); } } if (historyItems.Any()) { var firstHistoryItem = historyItems.First(); var grabbedEvent = historyItems.FirstOrDefault(v => v.EventType == EntityHistoryEventType.Grabbed); trackedDownload.Indexer = grabbedEvent?.Data?.GetValueOrDefault("indexer"); if (parsedBookInfo == null || trackedDownload.RemoteBook?.Author == null || trackedDownload.RemoteBook.Books.Empty()) { // Try parsing the original source title and if that fails, try parsing it as a special var historyAuthor = firstHistoryItem.Author; var historyBooks = new List { firstHistoryItem.Book }; parsedBookInfo = Parser.Parser.ParseBookTitle(firstHistoryItem.SourceTitle); if (parsedBookInfo != null) { trackedDownload.RemoteBook = _parsingService.Map(parsedBookInfo, firstHistoryItem.AuthorId, historyItems.Where(v => v.EventType == EntityHistoryEventType.Grabbed).Select(h => h.BookId) .Distinct()); } else { parsedBookInfo = Parser.Parser.ParseBookTitleWithSearchCriteria(firstHistoryItem.SourceTitle, historyAuthor, historyBooks); if (parsedBookInfo != null) { trackedDownload.RemoteBook = _parsingService.Map(parsedBookInfo, firstHistoryItem.AuthorId, historyItems.Where(v => v.EventType == EntityHistoryEventType.Grabbed).Select(h => h.BookId) .Distinct()); } } } if (trackedDownload.RemoteBook != null && Enum.TryParse(grabbedEvent?.Data?.GetValueOrDefault("indexerFlags"), true, out IndexerFlags flags)) { trackedDownload.RemoteBook.Release ??= new ReleaseInfo(); trackedDownload.RemoteBook.Release.IndexerFlags = flags; } } // Calculate custom formats if (trackedDownload.RemoteBook != null) { trackedDownload.RemoteBook.CustomFormats = _formatCalculator.ParseCustomFormat(trackedDownload.RemoteBook, downloadItem.TotalSize); } // Track it so it can be displayed in the queue even though we can't determine which artist it is for if (trackedDownload.RemoteBook == null) { _logger.Trace("No Book found for download '{0}'", trackedDownload.DownloadItem.Title); } } catch (Exception e) { _logger.Debug(e, "Failed to find book for " + downloadItem.Title); return null; } LogItemChange(trackedDownload, existingItem?.DownloadItem, trackedDownload.DownloadItem); _cache.Set(trackedDownload.DownloadItem.DownloadId, trackedDownload); return trackedDownload; } public List GetTrackedDownloads() { return _cache.Values.ToList(); } public void UpdateTrackable(List trackedDownloads) { var untrackable = GetTrackedDownloads().ExceptBy(t => t.DownloadItem.DownloadId, trackedDownloads, t => t.DownloadItem.DownloadId, StringComparer.CurrentCulture).ToList(); foreach (var trackedDownload in untrackable) { trackedDownload.IsTrackable = false; } } private void LogItemChange(TrackedDownload trackedDownload, DownloadClientItem existingItem, DownloadClientItem downloadItem) { if (existingItem == null || existingItem.Status != downloadItem.Status || existingItem.CanBeRemoved != downloadItem.CanBeRemoved || existingItem.CanMoveFiles != downloadItem.CanMoveFiles) { _logger.Debug("Tracking '{0}:{1}': ClientState={2}{3} ReadarrStage={4} Book='{5}' OutputPath={6}.", downloadItem.DownloadClientInfo.Name, downloadItem.Title, downloadItem.Status, downloadItem.CanBeRemoved ? "" : downloadItem.CanMoveFiles ? " (busy)" : " (readonly)", trackedDownload.State, trackedDownload.RemoteBook?.ParsedBookInfo, downloadItem.OutputPath); } } private void UpdateCachedItem(TrackedDownload trackedDownload) { var parsedEpisodeInfo = Parser.Parser.ParseBookTitle(trackedDownload.DownloadItem.Title); trackedDownload.RemoteBook = parsedEpisodeInfo == null ? null : _parsingService.Map(parsedEpisodeInfo, 0, new[] { 0 }); } private static TrackedDownloadState GetStateFromHistory(DownloadHistoryEventType eventType) { switch (eventType) { case DownloadHistoryEventType.DownloadImportIncomplete: return TrackedDownloadState.ImportFailed; case DownloadHistoryEventType.DownloadImported: return TrackedDownloadState.Imported; case DownloadHistoryEventType.DownloadFailed: return TrackedDownloadState.DownloadFailed; case DownloadHistoryEventType.DownloadIgnored: return TrackedDownloadState.Ignored; default: return TrackedDownloadState.Downloading; } } public void Handle(BookInfoRefreshedEvent message) { var needsToUpdate = false; foreach (var episode in message.Removed) { var cachedItems = _cache.Values.Where(t => t.RemoteBook?.Books != null && t.RemoteBook.Books.Any(e => e.Id == episode.Id)) .ToList(); if (cachedItems.Any()) { needsToUpdate = true; } cachedItems.ForEach(UpdateCachedItem); } if (needsToUpdate) { _eventAggregator.PublishEvent(new TrackedDownloadRefreshedEvent(GetTrackedDownloads())); } } public void Handle(AuthorDeletedEvent message) { var cachedItems = _cache.Values.Where(t => t.RemoteBook?.Author != null && t.RemoteBook.Author.Id == message.Author.Id) .ToList(); if (cachedItems.Any()) { cachedItems.ForEach(UpdateCachedItem); _eventAggregator.PublishEvent(new TrackedDownloadRefreshedEvent(GetTrackedDownloads())); } } } }