using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using NLog; using NzbDrone.Common.Instrumentation.Extensions; using NzbDrone.Core.Exceptions; using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers.Events; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.IndexerSearch { public interface IReleaseSearchService { Task Search(NewznabRequest request, List indexerIds, bool interactiveSearch); } public class ReleaseSearchService : IReleaseSearchService { private readonly IIndexerLimitService _indexerLimitService; private readonly IEventAggregator _eventAggregator; private readonly IIndexerFactory _indexerFactory; private readonly Logger _logger; public ReleaseSearchService(IEventAggregator eventAggregator, IIndexerFactory indexerFactory, IIndexerLimitService indexerLimitService, Logger logger) { _eventAggregator = eventAggregator; _indexerFactory = indexerFactory; _indexerLimitService = indexerLimitService; _logger = logger; } public Task Search(NewznabRequest request, List indexerIds, bool interactiveSearch) { return request.t switch { "movie" => MovieSearch(request, indexerIds, interactiveSearch), "music" => MusicSearch(request, indexerIds, interactiveSearch), "tvsearch" => TvSearch(request, indexerIds, interactiveSearch), "book" => BookSearch(request, indexerIds, interactiveSearch), _ => BasicSearch(request, indexerIds, interactiveSearch) }; } private async Task MovieSearch(NewznabRequest request, List indexerIds, bool interactiveSearch) { var searchSpec = Get(request, indexerIds, interactiveSearch); var imdbId = ParseUtil.GetImdbId(request.imdbid); searchSpec.ImdbId = imdbId?.ToString("D7"); searchSpec.TmdbId = request.tmdbid; searchSpec.TraktId = request.traktid; searchSpec.DoubanId = request.doubanid; searchSpec.Year = request.year; searchSpec.Genre = request.genre; var releases = await Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec); return new NewznabResults { Releases = DeDupeReleases(releases) }; } private async Task MusicSearch(NewznabRequest request, List indexerIds, bool interactiveSearch) { var searchSpec = Get(request, indexerIds, interactiveSearch); searchSpec.Artist = request.artist; searchSpec.Album = request.album; searchSpec.Label = request.label; searchSpec.Genre = request.genre; searchSpec.Track = request.track; searchSpec.Year = request.year; var releases = await Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec); return new NewznabResults { Releases = DeDupeReleases(releases) }; } private async Task TvSearch(NewznabRequest request, List indexerIds, bool interactiveSearch) { var searchSpec = Get(request, indexerIds, interactiveSearch); var imdbId = ParseUtil.GetImdbId(request.imdbid); searchSpec.ImdbId = imdbId?.ToString("D7"); searchSpec.Season = request.season; searchSpec.Episode = request.ep; searchSpec.TvdbId = request.tvdbid; searchSpec.TraktId = request.traktid; searchSpec.TmdbId = request.tmdbid; searchSpec.DoubanId = request.doubanid; searchSpec.RId = request.rid; searchSpec.TvMazeId = request.tvmazeid; searchSpec.Year = request.year; searchSpec.Genre = request.genre; var releases = await Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec); return new NewznabResults { Releases = DeDupeReleases(releases) }; } private async Task BookSearch(NewznabRequest request, List indexerIds, bool interactiveSearch) { var searchSpec = Get(request, indexerIds, interactiveSearch); searchSpec.Author = request.author; searchSpec.Title = request.title; searchSpec.Publisher = request.publisher; searchSpec.Year = request.year; searchSpec.Genre = request.genre; var releases = await Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec); return new NewznabResults { Releases = DeDupeReleases(releases) }; } private async Task BasicSearch(NewznabRequest request, List indexerIds, bool interactiveSearch) { var searchSpec = Get(request, indexerIds, interactiveSearch); var releases = await Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec); return new NewznabResults { Releases = DeDupeReleases(releases) }; } private TSpec Get(NewznabRequest query, List indexerIds, bool interactiveSearch) where TSpec : SearchCriteriaBase, new() { var spec = new TSpec { InteractiveSearch = interactiveSearch }; spec.Categories = query.cat != null ? query.cat.Split(',').Where(s => !string.IsNullOrWhiteSpace(s)).Select(int.Parse).ToArray() : Array.Empty(); spec.SearchTerm = query.q?.Trim(); spec.SearchType = query.t; spec.Limit = query.limit ?? 100; spec.Offset = query.offset ?? 0; spec.MinAge = query.minage; spec.MaxAge = query.maxage; spec.MinSize = query.minsize; spec.MaxSize = query.maxsize; spec.Source = query.source; spec.Host = query.host; spec.IndexerIds = indexerIds; return spec; } private async Task> Dispatch(Func> searchAction, SearchCriteriaBase criteriaBase) { var indexers = _indexerFactory.Enabled(); if (criteriaBase.IndexerIds is { Count: > 0 }) { indexers = indexers.Where(i => criteriaBase.IndexerIds.Contains(i.Definition.Id) || (criteriaBase.IndexerIds.Contains(-1) && i.Protocol == DownloadProtocol.Usenet) || (criteriaBase.IndexerIds.Contains(-2) && i.Protocol == DownloadProtocol.Torrent)) .ToList(); if (criteriaBase.InteractiveSearch && indexers.Count == 0) { _logger.Debug("Search failed due to all selected indexers being unavailable: {0}", string.Join(", ", criteriaBase.IndexerIds)); throw new SearchFailedException("Search failed due to all selected indexers being unavailable"); } } if (criteriaBase.Categories is { Length: > 0 }) { //Only query supported indexers indexers = indexers.Where(i => ((IndexerDefinition)i.Definition).Capabilities.Categories.SupportedCategories(criteriaBase.Categories).Any()).ToList(); if (indexers.Count == 0) { _logger.Debug("All provided categories are unsupported by selected indexers: {0}", string.Join(", ", criteriaBase.Categories)); return Array.Empty(); } } _logger.ProgressInfo("Searching indexer(s): [{0}] for {1}", string.Join(", ", indexers.Select(i => i.Definition.Name).ToList()), criteriaBase.ToString()); var tasks = indexers.Select(x => DispatchIndexer(searchAction, x, criteriaBase)); var batch = await Task.WhenAll(tasks); var reports = batch.SelectMany(x => x).ToList(); _logger.Debug("Total of {0} reports were found for {1} from {2} indexer(s)", reports.Count, criteriaBase, indexers.Count); return reports; } private async Task> DispatchIndexer(Func> searchAction, IIndexer indexer, SearchCriteriaBase criteriaBase) { if (_indexerLimitService.AtQueryLimit((IndexerDefinition)indexer.Definition)) { return Array.Empty(); } try { var indexerReports = await searchAction(indexer); var releases = indexerReports.Releases; //Filter results to only those in searched categories if (criteriaBase.Categories.Length > 0) { var expandedQueryCats = ((IndexerDefinition)indexer.Definition).Capabilities.Categories.ExpandTorznabQueryCategories(criteriaBase.Categories); releases = releases.Where(result => result.Categories?.Any() != true || expandedQueryCats.Intersect(result.Categories.Select(c => c.Id)).Any()).ToList(); if (releases.Count != indexerReports.Releases.Count) { _logger.Trace("{0} releases from {1} ({2}) which didn't contain search categories [{3}] were filtered", indexerReports.Releases.Count - releases.Count, ((IndexerDefinition)indexer.Definition).Name, indexer.Name, string.Join(", ", expandedQueryCats)); } } if (criteriaBase.MinAge is > 0) { var cutoffDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(criteriaBase.MinAge.Value)); releases = releases.Where(r => r.PublishDate <= cutoffDate).ToList(); } if (criteriaBase.MaxAge is > 0) { var cutoffDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(criteriaBase.MaxAge.Value)); releases = releases.Where(r => r.PublishDate >= cutoffDate).ToList(); } if (criteriaBase.MinSize is > 0) { var minSize = criteriaBase.MinSize.Value; releases = releases.Where(r => r.Size >= minSize).ToList(); } if (criteriaBase.MaxSize is > 0) { var maxSize = criteriaBase.MaxSize.Value; releases = releases.Where(r => r.Size <= maxSize).ToList(); } foreach (var query in indexerReports.Queries) { _eventAggregator.PublishEvent(new IndexerQueryEvent(indexer.Definition.Id, criteriaBase, query)); } return releases; } catch (Exception e) { _eventAggregator.PublishEvent(new IndexerQueryEvent(indexer.Definition.Id, criteriaBase, new IndexerQueryResult())); _logger.Error(e, "Error while searching for {0}", criteriaBase); } return Array.Empty(); } private List DeDupeReleases(IList releases) { // De-dupe reports by guid so duplicate results aren't returned. Pick the one with the higher indexer priority. return releases.GroupBy(r => r.Guid) .Select(r => r.OrderBy(v => v.IndexerPriority).First()) .ToList(); } } }