using System; using System.Collections.Generic; using System.Linq; using FluentValidation.Results; using NLog; using NzbDrone.Common; using NzbDrone.Common.Http; using NzbDrone.Core.Indexers.Exceptions; using NzbDrone.Core.ThingiProvider; namespace NzbDrone.Core.Indexers.Newznab { public class Newznab : IndexerBase { private readonly IFetchFeedFromIndexers _feedFetcher; private readonly HttpProvider _httpProvider; private readonly Logger _logger; public Newznab(IFetchFeedFromIndexers feedFetcher, HttpProvider httpProvider, Logger logger) { _feedFetcher = feedFetcher; _httpProvider = httpProvider; _logger = logger; } //protected so it can be mocked, but not used for DI //TODO: Is there a better way to achieve this? protected Newznab() { } public override DownloadProtocol Protocol { get { return DownloadProtocol.Usenet; } } public override Int32 SupportedPageSize { get { return 100; } } public override IParseFeed Parser { get { return new NewznabParser(); } } public override IEnumerable DefaultDefinitions { get { var list = new List(); list.Add(GetDefinition("Nzbs.org", GetSettings("http://nzbs.org", 5000))); list.Add(GetDefinition("Nzb.su", GetSettings("https://api.nzb.su"))); list.Add(GetDefinition("Dognzb.cr", GetSettings("https://api.dognzb.cr"))); list.Add(GetDefinition("OZnzb.com", GetSettings("https://www.oznzb.com"))); list.Add(GetDefinition("nzbplanet.net", GetSettings("https://nzbplanet.net"))); list.Add(GetDefinition("NZBgeek", GetSettings("https://api.nzbgeek.info"))); return list; } } public override ProviderDefinition Definition { get; set; } public override IEnumerable RecentFeed { get { var categories = String.Join(",", Settings.Categories.Concat(Settings.AnimeCategories)); var url = String.Format("{0}/api?t=tvsearch&cat={1}&extended=1{2}", Settings.Url.TrimEnd('/'), categories, Settings.AdditionalParameters); if (!String.IsNullOrWhiteSpace(Settings.ApiKey)) { url += "&apikey=" + Settings.ApiKey; } yield return url; } } public override IEnumerable GetEpisodeSearchUrls(List titles, int tvRageId, int seasonNumber, int episodeNumber) { if (Settings.Categories.Empty()) { return Enumerable.Empty(); } if (tvRageId > 0) { return RecentFeed.Select(url => String.Format("{0}&limit=100&rid={1}&season={2}&ep={3}", url, tvRageId, seasonNumber, episodeNumber)); } return titles.SelectMany(title => RecentFeed.Select(url => String.Format("{0}&limit=100&q={1}&season={2}&ep={3}", url, NewsnabifyTitle(title), seasonNumber, episodeNumber))); } public override IEnumerable GetDailyEpisodeSearchUrls(List titles, int tvRageId, DateTime date) { if (Settings.Categories.Empty()) { return Enumerable.Empty(); } if (tvRageId > 0) { return RecentFeed.Select(url => String.Format("{0}&limit=100&rid={1}&season={2:yyyy}&ep={2:MM}/{2:dd}", url, tvRageId, date)).ToList(); } return titles.SelectMany(title => RecentFeed.Select(url => String.Format("{0}&limit=100&q={1}&season={2:yyyy}&ep={2:MM}/{2:dd}", url, NewsnabifyTitle(title), date)).ToList()); } public override IEnumerable GetAnimeEpisodeSearchUrls(List titles, int tvRageId, int absoluteEpisodeNumber) { if (Settings.AnimeCategories.Empty()) { return Enumerable.Empty(); } return titles.SelectMany(title => RecentFeed.Select(url => String.Format("{0}&limit=100&q={1}+{2:00}", url.Replace("t=tvsearch", "t=search"), NewsnabifyTitle(title), absoluteEpisodeNumber))); } public override IEnumerable GetSeasonSearchUrls(List titles, int tvRageId, int seasonNumber, int offset) { if (Settings.Categories.Empty()) { return Enumerable.Empty(); } if (tvRageId > 0) { return RecentFeed.Select(url => String.Format("{0}&limit=100&rid={1}&season={2}&offset={3}", url, tvRageId, seasonNumber, offset)); } return titles.SelectMany(title => RecentFeed.Select(url => String.Format("{0}&limit=100&q={1}&season={2}&offset={3}", url, NewsnabifyTitle(title), seasonNumber, offset))); } public override IEnumerable GetSearchUrls(string query, int offset) { // encode query (replace the + with spaces first) query = query.Replace("+", " "); query = System.Web.HttpUtility.UrlEncode(query); return RecentFeed.Select(url => String.Format("{0}&offset={1}&limit=100&q={2}", url.Replace("t=tvsearch", "t=search"), offset, query)); } public override ValidationResult Test() { var releases = _feedFetcher.FetchRss(this); if (releases.Any()) return new ValidationResult(); try { var url = RecentFeed.First(); var xml = _httpProvider.DownloadString(url); NewznabPreProcessor.Process(xml, url); } catch (ApiKeyException) { _logger.Warn("Indexer returned result for Newznab RSS URL, API Key appears to be invalid"); var apiKeyFailure = new ValidationFailure("ApiKey", "Invalid API Key"); return new ValidationResult(new List { apiKeyFailure }); } catch (RequestLimitReachedException) { _logger.Warn("Request limit reached"); } catch (Exception ex) { _logger.WarnException("Unable to connect to indexer: " + ex.Message, ex); var failure = new ValidationFailure("Url", "Unable to connect to indexer, check the log for more details"); return new ValidationResult(new List { failure }); } return new ValidationResult(); } private IndexerDefinition GetDefinition(String name, NewznabSettings settings) { return new IndexerDefinition { EnableRss = false, EnableSearch = false, Name = name, Implementation = GetType().Name, Settings = settings, Protocol = DownloadProtocol.Usenet, SupportsRss = SupportsRss, SupportsSearch = SupportsSearch }; } private NewznabSettings GetSettings(String url, params int[] categories) { var settings = new NewznabSettings { Url = url }; if (categories.Any()) { settings.Categories = categories; } return settings; } private static string NewsnabifyTitle(string title) { return title.Replace("+", "%20"); } } }